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
24 changes: 24 additions & 0 deletions CONSTITUTION_CONFORMANCE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
# Constitution conformance record

## Issue #69 assessment — 2026-09-11

This assessment extends and supersedes the prior issue #68 scope below for the
current candidate. Constitution v1.0.0 at
`a9cc8a503aa30e17820edc62ac95f7cbe10e0564` remains the authority.
Accountable owner: @andrewperis; status: Conforming candidate pending owner merge.
Base revision: `07abeef7bd02ae63c5bc1505a0d02a20f680a0bc`.

Scope: Execution provenance builder/validator, compiled pinned schema, optional executor observer, bounded local observer, docs, ADR 0007 and synthetic example.
Evidence: npm test: 78 passing tests. Codex valid/invalid provenance fixtures and deterministic validator generation checked.
Codex PR #9 review constraints are consumed from commit
`416f0493d5b7932b818387ea23030f6d0f89c395`; compiled validation enables RFC 3339
format assertions and rejects contradictory records even with recomputed digests.
PR #12 review hardening adds request/result target, classification and output
contract checks, including all classification downgrade pairs and fail-closed
observer behavior for a target-mismatch preflight result.
No new constitutional exceptions. Public fixtures are synthetic. Body content,
authorization and diagnostic strings are omitted; non-public content identities
are suppressed. Sinks cannot change the provider result or trigger retries.
Caller-owned opaque identifiers, production sink retention/access controls and
immutable contract publication (#72) remain outside this candidate.
The existing lockfile has a high-severity fast-uri advisory reported by npm audit; dependency remediation remains separate, with no dependency changes in this diff.
Next review: before release or any security-boundary/dependency change.

## Constitutional alignment

- Constitution: [Definitely Secure Studio Constitution v1.0.0](https://github.com/DefinitelySecureStudio/studio/tree/constitution/v1.0.0)
Expand Down
3 changes: 3 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Platform documentation

- [Execution provenance](execution-provenance.md) covers metadata policy and pluggable observers.
- [Observer decision](adr/0007-execution-observers.md) records the failure-isolated boundary.

This directory contains architecture decisions, public interfaces, operational
guides, and deployment documentation for the production platform.

Expand Down
15 changes: 15 additions & 0 deletions docs/adr/0007-execution-observers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# ADR 0007: Execution provenance observers

Candidate for owner merge, 2026-09-11. Owner: @andrewperis.
Issue: https://github.com/DefinitelySecureStudio/studio/issues/69

Project accepted execution results into the Codex Execution Provenance v1
allowlist and deliver once to an optional vendor-neutral observer. Capture
effective parameters at negotiation, before adapter invocation. Use immutable
snapshots and bounded sink waiting so observability cannot obscure results.
Expose a pure builder for later validation/structured-output revisions.

Default storage is a bounded memory buffer. Body capture is excluded even when
execution policy allows it. Classification gates hashes/context identity, and
diagnostics become counts. This preserves traceability while keeping free-form
provider and prompt data out of sinks. Custom sinks own their storage policy.
56 changes: 56 additions & 0 deletions docs/execution-provenance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Execution provenance and observers

Issue #69 adds a metadata record suitable for linking execution/correlation ids
to later build manifests. Enable it with `executePrompt(request, { adapter,
observer: new LocalExecutionObserver() })`. No observer means no record storage.

Observers implement `async observe(record)`. The executor passes one frozen
record after an accepted execution with matching request/result identity finishes, including preflight failure,
cancellation and timeout. Observer rejection, mutation attempts and deadline
expiry add `OBSERVATION_DELIVERY_FAILED` to the result. Provider status is
preserved and no execution is retried. The default observer deadline is 1000 ms;
`observerTimeoutMs` accepts 1–60000 ms. Timeout bounds waiting, but cannot cancel
external work a custom sink already started. Sinks must manage their own I/O.

`LocalExecutionObserver({ capacity: 100 })` keeps the newest records in memory.
`snapshot()` returns copies; `clear()` erases the local buffer. It performs no
console/file/network output. Production sinks own retention and access control.

`createExecutionProvenance(request, result, options)` supports post-execution
construction and later structured-output evidence. Options are
`effectiveParameters`, `validation`, `structuredOutput`, and
`contentIdentities` (`public-only`, default, or `omit`). Effective parameters are
captured automatically by the executor after negotiation. Manual callers must
provide the resolved values or the record states `not-resolved`.

Manual construction rejects mismatched execution/correlation ids, target
adapter/provider/model ids, output kind/media type, and any output classification
below the rendered prompt. A matching id alone is not proof of origin; callers
still own the authenticity of the supplied evidence. Target-mismatch preflight
failures retain their original execution error, but cannot produce a matching
provenance record: the executor adds its static delivery warning instead.

Pass prompt validation/lint evidence as `provenance: { validation: report }` to
the executor. Only outcome/counts survive; absent evidence is `not-run`.
After structured processing, construct another record with `structuredOutput`
and send it to the same observer. Processing status/id are retained only after
matching execution id, expectation and raw byte identity. Keep both record
revisions if an audit requires history; their canonical digests differ.

Records omit messages, input/output bodies, parsed values, URIs, authorization
details, provider request ids, idempotency keys, stop strings, extension objects,
and diagnostic/error/warning text. Numeric portable parameters and stop count
remain. Render/output hashes and context package/source versions are retained
only for public content; context identities are suppressed when the overall
rendered prompt is non-public. `omit` suppresses all content identities.
Body-capture permission never broadens this metadata projection.

Prompt, execution, correlation and provider/model ids must be approved opaque
identifiers. A secret disguised as an identifier cannot be detected by a schema.
Records are operational evidence, not an automatic public release artifact.

The closed Codex schema is pinned by the reproducible generator:
`node scripts/generate-provenance-schema-validator.mjs /path/to/execution-provenance.schema.json`.
The envelope identity covers Studio canonical JSON of `record`. Consumers use
`validateExecutionProvenance` to check schema and digest. Immutable contract
publication under issue #72 remains required before a production release.
15 changes: 15 additions & 0 deletions examples/execution-provenance.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { readFile } from 'node:fs/promises';
import { renderPrompt, createExecutionRequest, executePrompt, MockTextAdapter, LocalExecutionObserver } from '../src/prompt-sdk/index.js';

const definition = JSON.parse(await readFile(new URL('../tests/fixtures/prompt-definition.json', import.meta.url)));
const request = createExecutionRequest(renderPrompt(definition, { inputValues: { item: 'blue cube', attributes: {} } }), {
execution_id: 'exec_provenance_example', correlation_id: 'build_example', idempotency_key: 'idem_example',
target: { adapter_id: 'studio.mock.text', provider_id: 'studio-mock', model_id: 'mock-text-v1' },
capabilities: { required: ['text-generation'], optional: [] }, parameters: {},
expected_output: { kind: 'text', media_type: 'text/plain', validation: 'none' },
delegation: { caller_id: 'example', human_owner: 'andrewperis', purpose: 'Synthetic provenance example.', authority_reference: 'https://github.com/DefinitelySecureStudio/studio/issues/69' },
observability: { retention: 'metadata-only', capture_prompt: false, capture_output: false }
});
const observer = new LocalExecutionObserver();
await executePrompt(request, { adapter: new MockTextAdapter(), observer, clock: () => 0 });
console.log(JSON.stringify(observer.snapshot()[0], null, 2));
13 changes: 13 additions & 0 deletions scripts/generate-provenance-schema-validator.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { readFile, writeFile } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import Ajv from 'ajv/dist/2020.js';
import addFormats from 'ajv-formats';
import standaloneCode from 'ajv/dist/standalone/index.js';
const bytes = await readFile(process.argv[2]);
const commit = '416f0493d5b7932b818387ea23030f6d0f89c395';
const hash = 'd1f897fdd40c8a513f2f3dc9c44728ec124e53a57977a1b11b04e00953020118';
if (bytes.length !== 13143 || createHash('sha256').update(bytes).digest('hex') !== hash) throw new Error('Provenance schema identity mismatch');
const ajv = new Ajv({ allErrors: true, strict: true, strictRequired: false, code: { source: true, esm: true } });
addFormats(ajv);
const code = standaloneCode(ajv, ajv.compile(JSON.parse(bytes)));
await writeFile(new URL('../src/prompt-sdk/generated/provenance-v1-schema.js', import.meta.url), `// Generated from DefinitelySecureStudio/codex@${commit}; SHA256 ${hash}\nimport { createRequire } from 'node:module';\nconst require = createRequire(import.meta.url);\n${code}`);
22 changes: 21 additions & 1 deletion src/prompt-sdk/execute.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createHash } from "node:crypto";
import { AdapterExecutionError, ExecutionValidationError } from "./execution-errors.js";
import { validateExecutionCompatibility, validateExecutionDocument, validateExecutionResult } from "./validate-execution.js";
import { createExecutionProvenance, deliverExecutionProvenance } from './provenance.js';

function identity(descriptor, request) {
return descriptor ? {
Expand Down Expand Up @@ -52,7 +53,7 @@ function normalizeThrown(error) {
return { category: "internal", code: "ADAPTER_UNEXPECTED_ERROR", message: "The adapter failed without a normalized error.", retryable: false, stage: "adapter" };
}

export async function executePrompt(request, { adapter, signal, clock = Date.now } = {}) {
async function executePromptCore(request, { adapter, signal, clock = Date.now } = {}, observation = {}) {
if (!adapter || typeof adapter.describe !== "function" || typeof adapter.execute !== "function") throw new TypeError("adapter must implement describe() and execute().");
const requestValidation = validateExecutionDocument(request);
if (!requestValidation.valid) throw new ExecutionValidationError("Execution request does not satisfy the provider-neutral contract.", requestValidation);
Expand All @@ -66,6 +67,7 @@ export async function executePrompt(request, { adapter, signal, clock = Date.now
const compatibility = validateExecutionCompatibility(request, descriptor);
const preflightWarnings = compatibility.diagnostics.filter(({ severity }) => severity === "warning").map(warning);
if (!compatibility.valid) return failure(request, descriptor, started, clock(), compatibilityError(), preflightWarnings);
observation.effectiveParameters = structuredClone(compatibility.effectiveParameters);
if (signal?.aborted) return failure(request, descriptor, started, clock(), { category: "cancelled", code: "EXECUTION_CANCELLED", message: "Execution was cancelled before provider invocation.", retryable: false, stage: "preflight" }, preflightWarnings);

const controller = new AbortController();
Expand Down Expand Up @@ -116,3 +118,21 @@ export async function executePrompt(request, { adapter, signal, clock = Date.now
signal?.removeEventListener("abort", abort);
}
}

export async function executePrompt(request, options = {}) {
const { observer, observerTimeoutMs = 1000, provenance = {} } = options;
if (observer === undefined) return executePromptCore(request, options);
if (typeof observer?.observe !== 'function' || !Number.isSafeInteger(observerTimeoutMs) || observerTimeoutMs < 1 || observerTimeoutMs > 60000) throw new TypeError('Invalid observer or observer timeout');
const requestValidation = validateExecutionDocument(request);
if (!requestValidation.valid) throw new ExecutionValidationError('Execution request does not satisfy the provider-neutral contract.', requestValidation);
const snapshot = structuredClone(request);
const observation = {};
const result = await executePromptCore(structuredClone(snapshot), options, observation);
let delivered = false;
try {
const document = createExecutionProvenance(snapshot, result, { ...provenance, effectiveParameters: observation.effectiveParameters });
delivered = await deliverExecutionProvenance(observer, document, observerTimeoutMs);
} catch { /* Observer failures must not obscure the provider outcome. */ }
if (!delivered) return { ...result, warnings: [...result.warnings, { code: 'OBSERVATION_DELIVERY_FAILED', message: 'Execution provenance could not be delivered.' }] };
return result;
}
4 changes: 4 additions & 0 deletions src/prompt-sdk/generated/provenance-v1-schema.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/prompt-sdk/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { canonicalJson } from "./canonical-json.js";
export { createExecutionProvenance, validateExecutionProvenance, LocalExecutionObserver, deliverExecutionProvenance } from './provenance.js';
export { CONTEXT_PACKAGE_CONTRACT, CONTRACT, EXECUTION_CONTRACT, PROMPT_DEFINITION_CONTRACT, STRUCTURED_OUTPUT_CONTRACT } from "./diagnostics.js";
export { AdapterExecutionError, ExecutionValidationError } from "./execution-errors.js";
export { ContextPackageError } from "./context-errors.js";
Expand Down
Loading