Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 0.14.0 - 2026-07-08

### New Exports

- **`toAuditChainWriteOptions(envelope, options?)`** (`src/contracts/attestation-envelope.ts`) — bridges `AttestationEnvelope` into `@stackbilt/audit-chain`'s `writeRecord()` options. `AttestationEnvelope` and audit-chain's `AuditRecord` are different layers, not the same shape: the 0.5.0 CHANGELOG entry describing `AttestationEnvelope` as the "canonical shape for... audit-chain entries" was aspirational and, as of this release, still had zero real consumers — none of the four named instances (stackd, audit-chain, reading-envelope, evals) had adopted it, and audit-chain's actual `AuditRecord` (`record_id, namespace, event_type, hash, prev_hash, actor, timestamp, payload, metadata?`) shares no field vocabulary with the envelope (`traceId, tenantId, ticketRef, model, costUsd, result, sealedAt, hmac`). This function makes the envelope the record's `payload` rather than forcing a field-for-field merge, and derives sensible `namespace`/`actor` defaults from `tenantId`/`agentId` for the chain-identity fields the envelope has none for. `AuditChainWriteOptions` and `ToAuditChainOptions` types exported alongside it.

Surfaced while scoping a generalized rubric-contract primitive off of `@stackbilt/evidence-core`'s bespoke `src/audit/adapter.ts`, which independently reimplements this exact bridge (correctly, matching audit-chain's real shape) because no shared version existed to depend on.

## 0.13.0 - 2026-07-04

### New Exports
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@stackbilt/contracts",
"version": "0.13.0",
"version": "0.14.0",
"files": [
"dist",
"README.md",
Expand Down
55 changes: 55 additions & 0 deletions src/contracts/attestation-envelope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ import { z } from 'zod';
* (trace_id + tenant_id binding to prevent cross-tenant inference),
* evals GraderRun receipts.
*
* This schema is the sealed-payload shape, not audit-chain's own
* `AuditRecord` shape — the two are different layers (see
* `toAuditChainWriteOptions()` below for the bridge). As of 2026-07,
* no consumer has adopted this yet; each of the four instances above
* still has its own bespoke sealing logic.
*
* Goal: let audit-chain become the canonical sealing layer under stackd
* receipts and dispatch traces (Stackproof product primitive).
*/
Expand Down Expand Up @@ -52,3 +58,52 @@ export function canonicalPayload(e: AttestationEnvelope): string {
sealedAt: e.sealedAt,
});
}

/**
* `@stackbilt/audit-chain`'s `writeRecord()` options, minus the `bindings`
* and `chainHead` arguments a caller supplies separately. Duplicated here
* (not imported) to keep this package dependency-free — see
* `@stackbilt/audit-chain`'s `AuditRecord` / `writeRecord()` for the source
* of truth on this shape.
*/
export interface AuditChainWriteOptions {
namespace: string;
event_type: string;
actor: string;
payload: Record<string, unknown>;
metadata?: Record<string, unknown>;
}

export interface ToAuditChainOptions {
/** audit-chain namespace for this envelope's chain. Defaults to `tenant:{tenantId}`. */
namespace?: string;
/** audit-chain event_type. Defaults to `'attestation.sealed'`. */
eventType?: string;
/** audit-chain actor. Defaults to `envelope.agentId`, falling back to `'system'`. */
actor?: string;
}

/**
* Bridge an `AttestationEnvelope` into `@stackbilt/audit-chain`'s
* `writeRecord()` options.
*
* `AttestationEnvelope` and audit-chain's `AuditRecord` are different
* layers, not the same shape: the envelope is the sealed unit of work
* (result + provenance — traceId, tenantId, model, cost, hmac), while
* `AuditRecord` is the generic hash-chained log entry that wraps *any*
* event. This function makes the envelope the record's `payload` and
* derives sensible defaults for the chain-identity fields (`namespace`,
* `actor`) that the envelope has no equivalent for — it does not attempt
* to unify the two field vocabularies into one flat schema.
*/
export function toAuditChainWriteOptions(
envelope: AttestationEnvelope,
options: ToAuditChainOptions = {},
): AuditChainWriteOptions {
return {
namespace: options.namespace ?? `tenant:${envelope.tenantId}`,
event_type: options.eventType ?? 'attestation.sealed',
actor: options.actor ?? envelope.agentId ?? 'system',
payload: { ...envelope },
};
}
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,14 @@ export type { Posture, PostureSignal, PostureGateResult } from './contracts/post
export {
AttestationEnvelopeSchema,
canonicalPayload,
toAuditChainWriteOptions,
} from './contracts/attestation-envelope.js';

export type { AttestationEnvelope } from './contracts/attestation-envelope.js';
export type {
AttestationEnvelope,
AuditChainWriteOptions,
ToAuditChainOptions,
} from './contracts/attestation-envelope.js';

export {
CcTaskStatusSchema,
Expand Down
94 changes: 94 additions & 0 deletions tests/attestation-envelope.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { describe, expect, it } from 'vitest';
import {
AttestationEnvelopeSchema,
canonicalPayload,
toAuditChainWriteOptions,
} from '../src/index.js';

const envelope = {
envelopeId: '123e4567-e89b-12d3-a456-426614174000',
traceId: 'trace-abc',
tenantId: 'tenant-1',
ticketRef: 'github:Stackbilt-dev/aegis#42',
agentId: 'agent-7',
model: 'claude-sonnet-5',
provider: 'anthropic',
inputTokens: 100,
outputTokens: 50,
costUsd: 0.01,
result: { ok: true },
sealedAt: '2026-07-08T00:00:00.000Z',
};

describe('AttestationEnvelope', () => {
it('validates a canonical envelope shape', () => {
const result = AttestationEnvelopeSchema.safeParse(envelope);
expect(result.success).toBe(true);
});

it('produces a deterministic canonical payload', () => {
const payload = canonicalPayload(envelope);
expect(JSON.parse(payload)).toEqual({
envelopeId: envelope.envelopeId,
traceId: envelope.traceId,
tenantId: envelope.tenantId,
ticketRef: envelope.ticketRef,
agentId: envelope.agentId,
model: envelope.model,
provider: envelope.provider,
costUsd: envelope.costUsd,
sealedAt: envelope.sealedAt,
});
});

describe('toAuditChainWriteOptions', () => {
it('derives audit-chain identity fields from the envelope by default', () => {
const opts = toAuditChainWriteOptions(envelope);

expect(opts.namespace).toBe(`tenant:${envelope.tenantId}`);
expect(opts.event_type).toBe('attestation.sealed');
expect(opts.actor).toBe(envelope.agentId);
});

it('falls back to a system actor when agentId is absent', () => {
const { agentId: _agentId, ...withoutAgent } = envelope;
const opts = toAuditChainWriteOptions(withoutAgent);

expect(opts.actor).toBe('system');
});

it('lets the caller override namespace, event_type, and actor', () => {
const opts = toAuditChainWriteOptions(envelope, {
namespace: 'content:draft-9',
eventType: 'evidence.validation.completed',
actor: 'system:evidence-engine',
});

expect(opts.namespace).toBe('content:draft-9');
expect(opts.event_type).toBe('evidence.validation.completed');
expect(opts.actor).toBe('system:evidence-engine');
});

it('carries the full envelope as the record payload without mutation', () => {
const opts = toAuditChainWriteOptions(envelope);

expect(opts.payload).toEqual(envelope);
expect(opts.payload).not.toBe(envelope);
});

it('produces a payload that round-trips back through the envelope schema', () => {
const opts = toAuditChainWriteOptions(envelope);
const reparsed = AttestationEnvelopeSchema.safeParse(opts.payload);

expect(reparsed.success).toBe(true);
});

it('matches the shape of audit-chain writeRecord() options (minus bindings + chainHead)', () => {
const opts = toAuditChainWriteOptions(envelope);

expect(Object.keys(opts).sort()).toEqual(
['actor', 'event_type', 'namespace', 'payload'].sort(),
);
});
});
});