Skip to content
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 2.0.0 - 2026-07-13

- Publish CompiledBundle contract `1.1.0` with closed nested schemas for provenance, context, policy references, and plugin references.
- Add `integrity.canonicalInput` so an independent consumer can recompute the content hash and reject a context-mutated bundle before composition.
- Make the breaking strict-parser and compiler-output change explicit as a package major release. The immutable v1.0.1 package and contract remain available at their existing release tag; new integrations should select the CompiledBundle `1.1.0` schema identifier and ASTER package v2.

## 1.0.1 - 2026-07-13

- Release the versioned CompiledBundle schema and compiler-generated public fixture introduced during the v1.0 stabilization work.
Expand Down
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,22 @@ node node_modules/@aster/persona-contract-compiler/dist/apps/api/src/runtime.js
`@aster/persona-contract-compiler/contracts/schemas/compiled-bundle.schema.json` and a deterministic
compiler-produced fixture at
`@aster/persona-contract-compiler/contracts/fixtures/compiled-bundle.v1.json`.
The schema identifier is `https://tuzuminami.github.io/aster/contracts/compiled-bundle/1.0.0/schema.json`.
Consumers must treat `contentHash` as the SHA-256 identity of the canonical compiled context. Contract
The current schema identifier is `https://tuzuminami.github.io/aster/contracts/compiled-bundle/1.1.0/schema.json`.
`integrity.canonicalInput` is the complete public hash input: consumers recompute `contentHash` as SHA-256
of UTF-8 encoded ASTER Canonical JSON v1, then verify its persona ID/version against the outer bundle fields,
its full provenance against `provenance`, and its execution fields against `context`. ASTER Canonical JSON v1
recursively sorts object keys by JavaScript string code-unit order, preserves array order, omits undefined
object values, and serializes with `JSON.stringify`. That makes a bundle with a changed runtime context,
identity, provenance, or retained digest fail verification before it composes
with systems such as [DRIFT](https://github.com/tuzuminami/drift).

This is an internal integrity contract, not a transport-authenticity signature. A consumer receiving a bundle
from an untrusted transport must verify it with `parseVerifiedCompiledBundle` and pin or authenticate its
distribution channel separately.

The immutable v1.0.1 package and artifact remain available at their existing release tag for compatibility.
ASTER package v2 makes the stricter parser and compiler output explicit as a breaking API change. New independent
or integrated consumers should select the CompiledBundle v1.1.0 schema identifier and ASTER package v2. Contract
changes require a new schema identifier and an explicit compatibility policy; ASTER never creates a runtime
dependency on consumers such as DRIFT.

Expand Down Expand Up @@ -162,7 +176,7 @@ Primary flow:
4. `POST /v1/personas/{personaId}/versions/{version}/compile`
5. `GET /v1/personas/{personaId}/versions/{version}/diff/{otherVersion}`

The package and OpenAPI `info.version` are one release contract: ASTER `1.x.y` ships OpenAPI `1.x.y`.
The package and OpenAPI `info.version` are one release contract: ASTER `x.y.z` ships OpenAPI `x.y.z`.
The `/v1` HTTP path remains the separate API-major compatibility axis. `pnpm run check:release-contract`
rejects version drift before release. See `packages/contracts/openapi/openapi.yaml` and
`packages/contracts/schemas/persona-contract.schema.json`.
Expand Down
5 changes: 3 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

## Supported Versions

ASTER v1.x receives security fixes for the latest supported v1 release. Security fixes also
target the default branch while the next release is in development.
ASTER v2.x receives security fixes for the latest supported v2 release. ASTER v1.0.1 remains
available as the immutable prior contract release; security fixes also target the default branch
while the next release is in development.

## Reporting a Vulnerability

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": "@aster/persona-contract-compiler",
"version": "1.0.1",
"version": "2.0.0",
"private": false,
"license": "Apache-2.0",
"type": "module",
Expand Down
50 changes: 48 additions & 2 deletions packages/contracts/fixtures/compiled-bundle.v1.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,54 @@
{
"personaId": "persona_contract_fixture",
"version": 1,
"compilerVersion": "aster-compiler/0.1.0",
"contentHash": "528a4ed2da51e3ca105b704c442178ce6ac430f163cb4c425315f387b8efedc6",
"compilerVersion": "aster-compiler/0.2.0",
"contentHash": "c73be96db01b2a3eb918f43d7a6068e8eb3685e4255e078a884c6eb18a22d9c7",
"integrity": {
"algorithm": "sha256",
"canonicalization": "aster-canonical-json/1",
"encoding": "utf-8",
"canonicalInput": {
"personaId": "persona_contract_fixture",
"version": 1,
"compilerVersion": "aster-compiler/0.2.0",
"sourceContractHash": "b950347e15b00657e20c32ef1f4706fd3099ffc0118d5cf14629799ce80dcc31",
"compiledAt": "2026-07-13T00:00:00.000Z",
"componentIds": [
"base",
"boundary",
"context"
],
"policyReferenceIds": [
"default-safety@2026-01"
],
"pluginReferenceIds": [],
"persona": {
"displayName": "Aster Tutor",
"purpose": "Help a learner practice concise explanations.",
"voice": [
"calm",
"precise"
]
},
"instructions": [
"Answer with concrete examples."
],
"boundaries": [
"Do not claim real-world authority."
],
"contextBlocks": [
"Prefer short practice loops."
],
"policyReferences": [
{
"id": "default-safety",
"version": "2026-01",
"required": true
}
],
"pluginReferences": []
}
},
"provenance": {
"sourceContractHash": "b950347e15b00657e20c32ef1f4706fd3099ffc0118d5cf14629799ce80dcc31",
"compiledAt": "2026-07-13T00:00:00.000Z",
Expand Down
12 changes: 11 additions & 1 deletion packages/contracts/openapi/openapi.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: ASTER API
version: 1.0.1
version: 2.0.0
paths:
/health:
get:
Expand Down Expand Up @@ -107,6 +107,14 @@ paths:
responses:
"200":
description: Compiled bundle.
content:
application/json:
schema:
type: object
required: [data]
properties:
data:
$ref: "#/components/schemas/CompiledBundle"
"409":
description: Idempotency key was reused for a different request.
/v1/personas/{personaId}/versions/{version}/diff/{otherVersion}:
Expand Down Expand Up @@ -187,6 +195,8 @@ components:
schema:
type: string
schemas:
CompiledBundle:
$ref: "../schemas/compiled-bundle.schema.json"
PluginManifest:
type: object
required: [name, version, capabilities, coreApiVersion, enabled]
Expand Down
107 changes: 88 additions & 19 deletions packages/contracts/schemas/compiled-bundle.schema.json
Original file line number Diff line number Diff line change
@@ -1,40 +1,109 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://tuzuminami.github.io/aster/contracts/compiled-bundle/1.0.0/schema.json",
"$id": "https://tuzuminami.github.io/aster/contracts/compiled-bundle/1.1.0/schema.json",
"title": "ASTER CompiledBundle",
"type": "object",
"required": ["personaId", "version", "compilerVersion", "contentHash", "provenance", "context"],
"required": ["personaId", "version", "compilerVersion", "contentHash", "integrity", "provenance", "context"],
"properties": {
"personaId": { "type": "string", "minLength": 1 },
"version": { "type": "integer", "minimum": 1 },
"compilerVersion": { "type": "string", "const": "aster-compiler/0.1.0" },
"version": { "type": "integer", "minimum": 1, "maximum": 9007199254740991 },
"compilerVersion": { "type": "string", "const": "aster-compiler/0.2.0" },
"contentHash": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
"integrity": {
"type": "object",
"required": ["algorithm", "canonicalization", "encoding", "canonicalInput"],
"properties": {
"algorithm": { "type": "string", "const": "sha256" },
"canonicalization": { "type": "string", "const": "aster-canonical-json/1" },
"encoding": { "type": "string", "const": "utf-8" },
"canonicalInput": { "$ref": "#/$defs/canonicalInput" }
},
"additionalProperties": false
},
"provenance": {
"type": "object",
"required": ["sourceContractHash", "compiledAt", "componentIds", "policyReferenceIds", "pluginReferenceIds"],
"properties": {
"sourceContractHash": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
"compiledAt": { "type": "string", "format": "date-time" },
"componentIds": { "type": "array", "items": { "type": "string" } },
"policyReferenceIds": { "type": "array", "items": { "type": "string" } },
"pluginReferenceIds": { "type": "array", "items": { "type": "string" } }
}
"compiledAt": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]+)?(?:Z|[+-][0-9]{2}:[0-9]{2})$" },
"componentIds": { "$ref": "#/$defs/nonEmptyStringArray" },
"policyReferenceIds": { "$ref": "#/$defs/nonEmptyStringArray" },
"pluginReferenceIds": { "$ref": "#/$defs/nonEmptyStringArray" }
},
"additionalProperties": false
},
"context": {
"type": "object",
"required": ["displayName", "purpose", "instructions", "boundaries", "contextBlocks", "policyReferences", "pluginReferences"],
"properties": {
"displayName": { "type": "string" },
"purpose": { "type": "string" },
"instructions": { "type": "array", "items": { "type": "string" } },
"boundaries": { "type": "array", "items": { "type": "string" } },
"contextBlocks": { "type": "array", "items": { "type": "string" } },
"policyReferences": { "type": "array" },
"pluginReferences": { "type": "array" }
}
"displayName": { "$ref": "#/$defs/nonEmptyString" },
"purpose": { "$ref": "#/$defs/nonEmptyString" },
"instructions": { "$ref": "#/$defs/nonEmptyStringArray" },
"boundaries": { "$ref": "#/$defs/nonEmptyStringArray" },
"contextBlocks": { "$ref": "#/$defs/nonEmptyStringArray" },
"policyReferences": { "type": "array", "items": { "$ref": "#/$defs/policyReference" } },
"pluginReferences": { "type": "array", "items": { "$ref": "#/$defs/pluginReference" } }
},
"additionalProperties": false
}
},
"additionalProperties": false,
"x-aster-contract-version": "1.0.0",
"x-aster-compatibility": "Consumers may accept additive fields only after upgrading this schema identifier; contentHash is SHA-256 of ASTER canonical compiled context."
"$defs": {
"nonEmptyString": { "type": "string", "minLength": 1 },
"nonEmptyStringArray": { "type": "array", "items": { "$ref": "#/$defs/nonEmptyString" } },
"persona": {
"type": "object",
"required": ["displayName", "purpose", "voice"],
"properties": {
"displayName": { "$ref": "#/$defs/nonEmptyString" },
"purpose": { "$ref": "#/$defs/nonEmptyString" },
"voice": { "$ref": "#/$defs/nonEmptyStringArray" }
},
"additionalProperties": false
},
"policyReference": {
"type": "object",
"required": ["id", "version", "required"],
"properties": {
"id": { "$ref": "#/$defs/nonEmptyString" },
"version": { "$ref": "#/$defs/nonEmptyString" },
"required": { "type": "boolean" }
},
"additionalProperties": false
},
"pluginReference": {
"type": "object",
"required": ["name", "version", "capability"],
"properties": {
"name": { "$ref": "#/$defs/nonEmptyString" },
"version": { "$ref": "#/$defs/nonEmptyString" },
"capability": { "type": "string", "enum": ["context_injector", "renderer"] }
},
"additionalProperties": false
},
"canonicalInput": {
"type": "object",
"required": ["personaId", "version", "compilerVersion", "sourceContractHash", "compiledAt", "componentIds", "policyReferenceIds", "pluginReferenceIds", "persona", "instructions", "boundaries", "contextBlocks", "policyReferences", "pluginReferences"],
"properties": {
"personaId": { "$ref": "#/$defs/nonEmptyString" },
"version": { "type": "integer", "minimum": 1, "maximum": 9007199254740991 },
"compilerVersion": { "type": "string", "const": "aster-compiler/0.2.0" },
"sourceContractHash": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
"compiledAt": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]+)?(?:Z|[+-][0-9]{2}:[0-9]{2})$" },
"componentIds": { "$ref": "#/$defs/nonEmptyStringArray" },
"policyReferenceIds": { "$ref": "#/$defs/nonEmptyStringArray" },
"pluginReferenceIds": { "$ref": "#/$defs/nonEmptyStringArray" },
"persona": { "$ref": "#/$defs/persona" },
"instructions": { "$ref": "#/$defs/nonEmptyStringArray" },
"boundaries": { "$ref": "#/$defs/nonEmptyStringArray" },
"contextBlocks": { "$ref": "#/$defs/nonEmptyStringArray" },
"policyReferences": { "type": "array", "items": { "$ref": "#/$defs/policyReference" } },
"pluginReferences": { "type": "array", "items": { "$ref": "#/$defs/pluginReference" } }
},
"additionalProperties": false
}
},
"x-aster-contract-version": "1.1.0",
"x-aster-compatibility": "CompiledBundle 1.1.0 supersedes 1.0.0 for new integrations. Consumers must select an explicit schema identifier; v1.1 adds a closed integrity.canonicalInput that can reproduce contentHash without ASTER's source-contract store.",
"x-aster-content-hash": "contentHash is SHA-256 of UTF-8 encoded ASTER Canonical JSON v1 for integrity.canonicalInput. Canonical JSON v1 recursively sorts object keys by JavaScript string code-unit order, preserves array order, omits undefined object values, and serializes with JSON.stringify. Consumers must verify canonicalInput.personaId/version against the outer bundle fields, canonicalInput's provenance fields against provenance, and canonicalInput's execution fields against context. This is an integrity check, not a transport-authenticity signature."
}
5 changes: 4 additions & 1 deletion packages/core/src/canonical.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { createHash } from "node:crypto";

export const ASTER_CANONICALIZATION = "aster-canonical-json/1";
export const ASTER_INTEGRITY_ENCODING = "utf-8";

export const canonicalJson = (value: unknown): string => JSON.stringify(canonicalize(value));

export const sha256Hex = (value: unknown): string =>
createHash("sha256").update(typeof value === "string" ? value : canonicalJson(value)).digest("hex");
createHash("sha256").update(typeof value === "string" ? value : canonicalJson(value), "utf8").digest("hex");

const canonicalize = (value: unknown): unknown => {
if (Array.isArray(value)) {
Expand Down
Loading