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
65 changes: 57 additions & 8 deletions apps/demo/src/generate-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
type ScriptPolicy,
type SummonLayout,
type SurfaceCeiling,
type SurfaceContractView,
type SurfacePlan,
type ValidationCapability,
type ValidationComponent,
Expand Down Expand Up @@ -379,6 +380,7 @@ let currentShape: string | null = null;
let currentValidationSummary: string | null = null;
let currentRepairSummary: string | null = null;
let currentStreamHealth: string | null = null;
let currentSurfaceContractView: SurfaceContractView | null = null;
const acc = new SectionAccumulator();
let handle: SandboxHandle | null = null;
let policy: PolicyEngine | null = null;
Expand Down Expand Up @@ -785,6 +787,7 @@ function applyScenario(scenario: ShowcaseScenario) {
currentValidationSummary = null;
currentRepairSummary = null;
currentStreamHealth = null;
currentSurfaceContractView = null;
respawn(currentDirectionId, currentMode);
showWelcome();
updateEditControls();
Expand All @@ -806,14 +809,31 @@ function planText(plan: { purpose: string; runtime: string; data: string; author
].join(' · ');
}

function parseSurfaceContractView(value: unknown): SurfaceContractView | null {
if (!value || typeof value !== 'object') return null;
const contract = value as Partial<SurfaceContractView>;
if (!contract.surface || typeof contract.surface !== 'object') return null;
if (!Array.isArray(contract.tools) || !Array.isArray(contract.components)) return null;
if (!Array.isArray(contract.issues)) return null;
return contract as SurfaceContractView;
}

function renderContractSummary() {
const active = readActiveContract();
const requested = active.surfacePlan;
const hostTools = active.capabilityNames.length ? active.capabilityNames.join(', ') : 'none';
const components = active.componentNames?.length ? active.componentNames.join(', ') : 'none';
const contract = currentSurfaceContractView;
const hostTools = contract
? contract.tools.map((tool) => tool.name).join(', ') || 'none'
: active.capabilityNames.length ? active.capabilityNames.join(', ') : 'none';
const components = contract
? contract.components.map((component) => component.name).join(', ') || 'none'
: active.componentNames?.length ? active.componentNames.join(', ') : 'none';
const toolCount = contract?.tools.length ?? active.capabilityNames.length;
const componentCount = contract?.components.length ?? active.componentNames?.length ?? 0;
const validation = currentValidationSummary ?? 'pending';
const stream = currentStreamHealth ?? 'pending';
const effective = currentEffectiveSurfacePlan ? planText(currentEffectiveSurfacePlan) : 'pending';
const effectivePlan = contract?.surface.plan ?? currentEffectiveSurfacePlan;
const effective = effectivePlan ? planText(effectivePlan) : 'pending';
const provider = modelProviders.find((item) => item.id === active.modelProvider);
const selectedModel = active.generationModel
?? provider?.defaults?.generationModel
Expand All @@ -823,16 +843,16 @@ function renderContractSummary() {
?? provider?.defaults?.utilityModel
?? provider?.utilityModel
?? 'server default';
inspectorStatusEl.textContent = currentEffectiveSurfacePlan ? 'effective' : 'pending';
inspectorStatusEl.textContent = contract ? 'contract' : currentEffectiveSurfacePlan ? 'effective' : 'pending';
contractSummaryEl.innerHTML = '';
const rows = [
['provider', 'Model provider', provider ? `${provider.name} · ${selectedModel}` : 'server default', provider ? 'neutral' : 'pending'],
['utility', 'Utility model', selectedUtility, provider ? 'neutral' : 'pending'],
['requested', 'Requested surface config', planText(requested), 'neutral'],
['effective', 'Effective safety plan', effective, currentEffectiveSurfacePlan ? 'good' : 'pending'],
['grants', 'Allowed host tools', `${active.capabilityNames.length}: ${hostTools}`, active.capabilityNames.length ? 'neutral' : 'pending'],
['components', 'Trusted components', `${active.componentNames?.length ?? 0}: ${components}`, active.componentNames?.length ? 'good' : 'pending'],
['runtime', 'Runtime', `${active.mode} · scripts ${active.scriptPolicy}`, active.scriptPolicy === 'allow' ? 'warn' : 'neutral'],
['effective', 'Effective safety plan', effective, effectivePlan ? 'good' : 'pending'],
['grants', 'Allowed host tools', `${toolCount}: ${hostTools}`, toolCount ? 'neutral' : 'pending'],
['components', 'Trusted components', `${componentCount}: ${components}`, componentCount ? 'good' : 'pending'],
['runtime', 'Runtime', `${contract?.surface.mode ?? active.mode} · scripts ${contract?.surface.scriptPolicy ?? active.scriptPolicy}`, (contract?.surface.scriptPolicy ?? active.scriptPolicy) === 'allow' ? 'warn' : 'neutral'],
['validation', 'Validation', validation, validation !== 'pending' && !validation.startsWith('0/') ? 'warn' : validation === 'pending' ? 'pending' : 'good'],
['stream', 'Stream diagnostics', stream, stream.startsWith('complete') ? 'good' : stream === 'pending' ? 'pending' : 'warn'],
['repair', 'Validation retry', active.repair?.enabled ? (currentRepairSummary ?? 'on') : 'off', active.repair?.enabled ? 'warn' : 'pending'],
Expand Down Expand Up @@ -862,6 +882,7 @@ function clearEffectiveContractSummary() {
currentValidationSummary = null;
currentRepairSummary = null;
currentStreamHealth = null;
currentSurfaceContractView = null;
renderContractSummary();
}

Expand Down Expand Up @@ -1584,6 +1605,7 @@ interface SandboxTarget {
/** Fires when the server emits `/mode-upgraded`. Parent respawns; children no-op. */
onModeUpgrade?: () => void;
onSurfacePlan?: (plan: SurfacePlan) => void;
onSurfaceContract?: (contract: SurfaceContractView) => void;
onShape?: (shape: string) => void;
onTokenOverrides?: (applied: Array<{ token: string; value: string }>) => void;
onValidationSummary?: (value: unknown) => void;
Expand Down Expand Up @@ -1625,6 +1647,19 @@ function applyLineTo(target: SandboxTarget, line: ProtocolLine, context: Surface
}
return;
}
if (line.op === 'meta' && line.path === '/surface-contract') {
const contract = parseSurfaceContractView(line.value);
if (contract) {
target.onSurfaceContract?.(contract);
target.onLog(
'op-meta',
`surface contract → ${contract.tools.length} tool${contract.tools.length === 1 ? '' : 's'}, ${contract.components.length} component${contract.components.length === 1 ? '' : 's'}`,
);
} else {
target.onLog('op-meta', `surface contract → invalid ${JSON.stringify(line.value)}`);
}
return;
}
if (line.op === 'meta' && line.path === '/shape') {
const shape = typeof line.value === 'string' ? line.value : '';
if (shape) target.onShape?.(shape);
Expand Down Expand Up @@ -1922,6 +1957,12 @@ async function streamGenerationInto(target: SandboxTarget, opts: StreamOptions):
events.push({ kind: 'surface-plan', at: Date.now(), plan: surfacePlan });
}
}
if (line.path === '/surface-contract') {
const contract = parseSurfaceContractView(line.value);
if (contract && target.recordEvents) {
events.push({ kind: 'surface-contract', at: Date.now(), contract });
}
}
if (line.path === '/shape' && typeof line.value === 'string') {
shape = line.value;
}
Expand Down Expand Up @@ -1969,6 +2010,10 @@ function createParentTarget(active: ActiveContract): SandboxTarget {
currentEffectiveSurfacePlan = plan;
renderContractSummary();
},
onSurfaceContract: (contract) => {
currentSurfaceContractView = contract;
renderContractSummary();
},
onShape: (shape) => {
currentShape = shape;
renderContractSummary();
Expand Down Expand Up @@ -2116,6 +2161,7 @@ function replaySurface(envelope: SurfaceEnvelope) {
currentStreamHealth = envelope.streamGraph
? `${envelope.streamGraph.health.complete ? 'complete' : 'open'} · missing=${envelope.streamGraph.health.missingDeclared.length} blocked=${envelope.streamGraph.health.blockedCount} retried=${envelope.streamGraph.health.repairedCount}`
: null;
currentSurfaceContractView = null;
acc.reset();
for (const line of envelope.protocolLines) {
if (line.op !== 'meta') acc.applyDetailed(line);
Expand Down Expand Up @@ -2157,6 +2203,7 @@ async function generate(prompt: string) {
currentValidationSummary = null;
currentRepairSummary = null;
currentStreamHealth = null;
currentSurfaceContractView = null;
respawn(currentDirectionId, active.mode, active);
hideWelcome();
goBtn.disabled = true;
Expand Down Expand Up @@ -2474,6 +2521,8 @@ function summarize(ev: DevtoolsEvent): string {
return `sections=${ev.sections.length} missing=${ev.health.missingDeclared.length} skipped=${ev.health.skippedCount} retried=${ev.health.repairedCount}`;
case 'surface-plan':
return planText(ev.plan);
case 'surface-contract':
return `${ev.contract.tools?.length ?? 0} tool${ev.contract.tools?.length === 1 ? '' : 's'} · ${ev.contract.components?.length ?? 0} component${ev.contract.components?.length === 1 ? '' : 's'}`;
case 'render':
return `${ev.bytes.toLocaleString()} B`;
case 'component-sync':
Expand Down
15 changes: 11 additions & 4 deletions apps/server/src/generate-route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,19 +206,20 @@ test('api generate sends narrowed contract and stream meta shape through package
assert.equal(policyRequest.stream, true);
const policySystemText = policyRequest.system?.map((block) => block.text ?? '').join('\n') ?? '';
assert.match(policySystemText, /Search host-owned dinner data/);
assert.match(policySystemText, /Surface plan/);
assert.match(policySystemText, /Runtime: `declarative`/);
assert.match(policySystemText, /Data: `host-resource`/);
assert.match(policySystemText, /Surface contract/);
assert.match(policySystemText, /runtime=`declarative`/);
assert.match(policySystemText, /data=`host-resource`/);
assert.doesNotMatch(policySystemText, /Rules for scripts/);

const policyLines = policyBody
.trim()
.split(/\n/)
.filter(Boolean)
.map((raw) => JSON.parse(raw) as ProtocolLine);
assert.deepEqual(policyLines.slice(0, 4).map((line) => `${line.op} ${line.path}`), [
assert.deepEqual(policyLines.slice(0, 5).map((line) => `${line.op} ${line.path}`), [
'meta /surface-policy',
'meta /surface-plan',
'meta /surface-contract',
'meta /status',
'set /screen',
]);
Expand All @@ -231,6 +232,12 @@ test('api generate sends narrowed contract and stream meta shape through package
persistence: 'replayable',
});
assert.deepEqual((policyLines[1] as Extract<ProtocolLine, { op: 'meta' }>).value, surfacePlan);
const policyContract = (policyLines[2] as Extract<ProtocolLine, { op: 'meta' }>).value as {
tools?: Array<{ name: string }>;
components?: Array<{ name: string }>;
};
assert.deepEqual(policyContract.tools?.map((tool) => tool.name), ['search']);
assert.deepEqual(policyContract.components, []);

const ghostResponse = await fetch(`http://127.0.0.1:${appPort}/api/generate`, {
method: 'POST',
Expand Down
11 changes: 7 additions & 4 deletions docs/adoption/debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ These names are useful when maintaining Summon or writing a deeper adapter:
| --- | --- |
| `/surface-policy` | Host-owned public surface config selected for this run. |
| `/surface-plan` | Host-owned compiled safety plan selected for this run. |
| `/surface-contract` | Host-owned compact view of the selected policy, narrowed tools/resources, trusted components, optional layout, and compile issues. |
| `/shape` | Optional server-inferred response shape used to narrow direction exemplars. |
| `/token-overrides` | Resolved direction token overrides, including applied and rejected entries. |
| `/validation-summary` | Final grouped `ContractIssue` counts and examples. |
Expand Down Expand Up @@ -184,14 +185,16 @@ and look for:
the name, props, bounds, or registry compatibility failed. The event includes
a stable code such as `bounds-invalid`, `unknown-component`, `props-invalid`,
or `registry-missing`.
- `surface-contract` - host-owned compact contract view emitted by the server
for policy-backed generations.
- `surface-plan` - host-owned compiled safety plan.
- `stream-graph` - client-side stream diagnostics from `StreamGraph.snapshot()`.
- `sandbox-fatal` - bootstrap detected an unsafe sandbox configuration.

Healthy interactive runs usually show `surface-plan`, `protocol-line`, `render`,
`component-sync`, `intent-emitted`, `intent-dispatched`, `state-pushed`, and
`stream-graph` in that order after the user interacts with a component-backed
UI.
Healthy interactive policy-backed runs usually show `surface-plan`,
`surface-contract`, `protocol-line`, `render`, `component-sync`,
`intent-emitted`, `intent-dispatched`, `state-pushed`, and `stream-graph` in
that order after the user interacts with a component-backed UI.

## Reading Contract Issues

Expand Down
30 changes: 30 additions & 0 deletions docs/adoption/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,28 @@ Common configs:
Hosts choose the config before generation. The model may react to the compiled
safety details, but it cannot widen what the host allowed.

### Surface Contract View

When a server receives a `SurfacePolicy`, Summon also derives a read-only
`SurfaceContractView` from the compiled policy. This view gives prompts,
Devtools, replay/debug tooling, and host UIs one compact answer to "what can
this generated surface do?"

The view includes:

- The normalized host policy, compiled `SurfacePlan`, mode, and script policy.
- Narrowed host tools/resources, including triggers, schemas, state keys, result
schema, and surface data/authority.
- Narrowed trusted components, including prop schema, sizing, and surface
data/authority.
- Optional host layout slots.
- Any `ContractIssue[]` produced while compiling the surface policy.

`SurfaceContractView` is diagnostic and prompt-facing only. It is not a JSON UI
schema and it is not an authority source. Enforcement still lives in
`SurfacePolicy` compilation, runtime validators, sandbox grants,
`PolicyEngine`, and component prop validation.

## 4. Generate The Surface

The generation server should use `@anarchitecture/summon-server` for the
Expand Down Expand Up @@ -215,6 +237,13 @@ await runSurfaceGeneration({
});
```

For policy-backed runs, `runSurfaceGeneration()` emits host-owned metadata in
this order before model-authored output:

1. `/surface-policy` - the normalized host policy.
2. `/surface-plan` - the compiled safety plan.
3. `/surface-contract` - the compact derived `SurfaceContractView`.

To enable validation retries, pass
`repair: { enabled: true, provider, maxAttempts, maxTargets }`. The provider
receives the compiled prompt blocks and a single replacement prompt; return one
Expand Down Expand Up @@ -298,6 +327,7 @@ await consumeSurfaceStream(response.body!, {
mode: compiledPolicy.mode,
onMeta: (line) => {
if (line.path === '/status') renderStatus(String(line.value));
if (line.path === '/surface-contract') renderContractSummary(line.value);
},
onGraph: (snapshot) => {
events.push({ kind: 'stream-graph', at: Date.now(), health: snapshot.health });
Expand Down
7 changes: 7 additions & 0 deletions docs/adoption/package-consumption.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,11 @@ defineReactComponent({

```ts
import {
compileSurfaceContractView,
compileSurfacePolicy,
createComponentRegistry,
defineComponent,
type SurfaceContractView,
} from '@anarchitecture/summon';
import {
consumeSurfaceStream,
Expand All @@ -116,6 +118,11 @@ contracts.
mode and narrowed contracts that the server will enforce. Generation authority
comes from the explicit surface config the host submits.

`compileSurfaceContractView(surfacePolicy, catalogs)` returns the same
policy-derived compact view that the server emits as `/surface-contract` for
policy-backed runs. Use it for previews, Devtools panels, and replay summaries;
do not use it as an enforcement source.

```ts
const componentRegistry = createComponentRegistry([
defineComponent({
Expand Down
10 changes: 8 additions & 2 deletions docs/adoption/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,14 @@ with exact runtime, data, authority, persistence, and script policy for
validation and diagnostics. Shape describes visual composition; posture
describes the act; the surface config describes the public host decision.

Generated UI must not emit or widen `/surface-policy` or `/surface-plan`.
Those meta lines are host-owned diagnostics.
Summon also derives a `SurfaceContractView` from the compiled policy. It is a
compact diagnostic and prompt-facing view of the selected policy, narrowed host
tools/resources, narrowed trusted components, optional host layout slots, and
compile issues. It does not grant authority and it does not replace validators
or the `PolicyEngine`.

Generated UI must not emit or widen `/surface-policy`, `/surface-plan`, or
`/surface-contract`. Those meta lines are host-owned diagnostics.

## Trusted Host Components

Expand Down
Loading
Loading