Skip to content
This repository was archived by the owner on Aug 19, 2026. It is now read-only.

Commit eca6eb2

Browse files
authored
Merge pull request #11 from block/codex/surface-policy-simplification
Introduce SurfacePolicy front door
2 parents f3e71fa + 6f77d4a commit eca6eb2

37 files changed

Lines changed: 1207 additions & 380 deletions

.agents/skills/summon/SKILL.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ Follow this path unless the user explicitly asks for a runtime redesign:
2828

2929
```txt
3030
host capability registry
31-
-> SurfacePlan: purpose/runtime/data/authority/persistence
31+
-> SurfacePolicy: tier/grants/components/purpose/persistence
32+
-> compiled SurfacePlan: purpose/runtime/data/authority/persistence
3233
-> createCapabilityRegistry(...).toContract()
3334
-> compileSystemContracts()
3435
-> protocol hardener and repair feedback
@@ -37,8 +38,8 @@ host capability registry
3738
```
3839

3940
Capabilities are host-owned. The model sees the contract; the host owns
40-
handlers, network, credentials, state, grants, and the selected `SurfacePlan`.
41-
Generated artifacts must not emit or widen `/surface-plan`.
41+
handlers, network, credentials, state, grants, and the selected `SurfacePolicy`.
42+
Generated artifacts must not emit or widen `/surface-policy` or `/surface-plan`.
4243

4344
New generation servers should prefer `runSurfaceGeneration(input, emit)` from
4445
`@anarchitecture/summon-server`; `generateSurfaceStream()` remains available for
@@ -70,8 +71,9 @@ host approval adapter.
7071

7172
For generation failures, inspect `/error`, `/validation-summary`,
7273
`/validation-blocked`, `/repair-feedback`, `/repair-summary`,
73-
`/stream-graph-summary`, `/protocol-skip`, `/surface-plan`, `/shape`,
74-
`/token-overrides`, `/screen-synthesized`, and `/mode-upgraded`.
74+
`/stream-graph-summary`, `/protocol-skip`, `/surface-policy`,
75+
`/surface-plan`, `/shape`, `/token-overrides`, `/screen-synthesized`, and
76+
`/mode-upgraded`.
7577

7678
For client behavior, inspect Devtools events: `surface-plan`, `protocol-line`,
7779
`protocol-parse-error`, `sandbox-ready`, `render`, `intent-emitted`,
@@ -80,7 +82,7 @@ For client behavior, inspect Devtools events: `surface-plan`, `protocol-line`,
8082

8183
Use `ContractIssue` plus `hintsForContractIssue(issue)` when feeding validation
8284
problems back to a model or another agent. For surface problems, check whether
83-
the requested runtime, data source, or authority exceeds the selected
85+
the requested grant/component exceeds the selected `SurfacePolicy` or compiled
8486
`SurfacePlan`.
8587

8688
## Commands

README.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ Summon's supported integration path is intentionally narrow:
6363

6464
```txt
6565
host capability registry
66-
-> SurfacePlan: purpose/runtime/data/authority/persistence
66+
-> SurfacePolicy: tier/grants/components/purpose/persistence
67+
-> compiled SurfacePlan: purpose/runtime/data/authority/persistence
6768
-> createCapabilityRegistry(...).toContract()
6869
-> compileSystemContracts()
6970
-> protocol hardener + repair feedback
@@ -74,18 +75,16 @@ host capability registry
7475
No generated artifact gets to mint permissions for itself. Artifact-declared
7576
intents are advisory; execution is governed by host grants.
7677

77-
Surface planning is Summon's lifecycle layer. A host can declare the minimum
78-
safe surface across purpose, runtime, data, authority, and persistence before
79-
generation starts. The model sees that plan as a contract but cannot widen it.
80-
Prompt-based `suggestSurfacePlan()` output is only advisory host UI scaffolding;
81-
generation falls back to no host data or authority unless the host submits an
82-
explicit accepted `SurfacePlan`.
78+
Surface policy is Summon's lifecycle layer. A host declares the public tier,
79+
grants, trusted components, purpose, and persistence before generation starts.
80+
Summon compiles that policy into the stricter `SurfacePlan` contract the model
81+
sees and cannot widen.
8382

8483
## Demo Map
8584

8685
- `examples/surface-gallery` - first-run OSS gallery with curated live presets,
8786
compact host contracts, a sandboxed surface, and a small event strip.
88-
- `/generate.html` - contract cockpit with scenario grants, surface plans,
87+
- `/generate.html` - contract cockpit with scenario grants, compiled surface plans,
8988
static/declarative/scripted/worker tiers, component islands, host resources,
9089
token overrides, repair diagnostics, edit/replay, Ghost steering, Devtools,
9190
and stream graph events.

apps/demo/src/generate-main.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,28 @@ import {
1212
parseSurfaceEnvelope,
1313
type SurfaceEnvelope,
1414
} from '@anarchitecture/summon/envelope';
15-
import {
16-
parseTokenValues,
17-
SectionAccumulator,
18-
type ProtocolLine,
19-
type SummonLayout,
20-
type ValidationCapability,
21-
type ValidationComponent,
22-
} from '@anarchitecture/summon/engine';
2315
import {
2416
deriveSurfacePlanControls,
2517
normalizeSurfacePlan,
26-
PolicyEngine,
18+
parseTokenValues,
19+
SectionAccumulator,
2720
SURFACE_AUTHORITY_VALUES,
2821
SURFACE_DATA_VALUES,
2922
SURFACE_PERSISTENCE_VALUES,
3023
SURFACE_PURPOSE_VALUES,
3124
SURFACE_RUNTIME_VALUES,
3225
type CapabilityPack,
3326
type ComponentPack,
27+
type ProtocolLine,
3428
type ScriptPolicy,
29+
type SummonLayout,
3530
type SurfaceCeiling,
3631
type SurfacePlan,
32+
type ValidationCapability,
33+
type ValidationComponent,
34+
} from '@anarchitecture/summon/engine';
35+
import {
36+
PolicyEngine,
3737
} from '@anarchitecture/summon';
3838
import { createEventStore, type DevtoolsEvent } from '@anarchitecture/summon/devtools';
3939
import bootstrapSource from '@anarchitecture/summon/bootstrap.js?raw';

apps/demo/src/showcase.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import assert from 'node:assert/strict';
22
import test from 'node:test';
3-
import { deriveSurfacePlanControls } from '@anarchitecture/summon';
3+
import { deriveSurfacePlanControls } from '@anarchitecture/summon/engine';
44
import {
55
createScopedDemoRegistry,
66
narrowCapabilityPack,

apps/demo/src/showcase.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type {
33
ScriptPolicy,
44
SurfacePlan,
55
SurfacePlanMode,
6-
} from '@anarchitecture/summon';
6+
} from '@anarchitecture/summon/engine';
77
import type { CapabilityRegistry } from '@anarchitecture/summon';
88
import { createDemoCapabilityRegistry, type DemoHandlerOptions } from './capabilities.js';
99

apps/server/src/capability-pack.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,11 @@ export function parseCapabilityPack(raw: unknown): CapabilityPack | null {
5555
if (!r || typeof r !== 'object') continue;
5656
const p = r as Record<string, unknown>;
5757
if (typeof p.name !== 'string' || typeof p.code !== 'string') continue;
58-
patterns.push({ name: p.name.slice(0, 100), code: p.code.slice(0, 4000) });
58+
patterns.push({
59+
name: p.name.slice(0, 100),
60+
code: p.code.slice(0, 4000),
61+
...(typeof p.intent === 'string' && p.intent ? { intent: p.intent.slice(0, 40) } : {}),
62+
});
5963
if (patterns.length >= 12) break;
6064
}
6165
}

apps/server/src/generate-route.test.ts

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@ import { createServer, type IncomingMessage } from 'node:http';
66
import { dirname, resolve } from 'node:path';
77
import { fileURLToPath } from 'node:url';
88
import test from 'node:test';
9-
import type { CapabilityPack, SurfaceCeiling, SurfacePlan } from '@anarchitecture/summon';
10-
import type { ProtocolLine } from '@anarchitecture/summon/engine';
9+
import type {
10+
CapabilityPack,
11+
ProtocolLine,
12+
SurfaceCeiling,
13+
SurfacePlan,
14+
} from '@anarchitecture/summon/engine';
1115

1216
const here = dirname(fileURLToPath(import.meta.url));
1317
const packageRoot = resolve(here, '..');
@@ -171,6 +175,55 @@ test('api generate sends narrowed contract and stream meta shape through package
171175
assert.equal((lines[1] as Extract<ProtocolLine, { op: 'meta' }>).value, 'writing');
172176
assert.equal(lines.at(-1)?.path, '/stream-graph-summary');
173177
assert.equal(lines.some((line) => line.path === '/error'), false);
178+
179+
const policyResponse = await fetch(`http://127.0.0.1:${appPort}/api/generate`, {
180+
method: 'POST',
181+
headers: { 'content-type': 'application/json' },
182+
body: JSON.stringify({
183+
prompt: 'build a dinner finder where i can search',
184+
mode: 'static',
185+
scriptPolicy: 'allow',
186+
surfacePolicy: {
187+
tier: 'declarative',
188+
purpose: 'explore',
189+
grants: ['search'],
190+
},
191+
capabilities: searchCapability,
192+
}),
193+
});
194+
const policyBody = await policyResponse.text();
195+
assert.equal(policyResponse.status, 200, policyBody);
196+
197+
assert.equal(anthropicRequests.length, 2);
198+
const policyRequest = anthropicRequests[1] as { system?: Array<{ text?: string }>; stream?: boolean };
199+
assert.equal(policyRequest.stream, true);
200+
const policySystemText = policyRequest.system?.map((block) => block.text ?? '').join('\n') ?? '';
201+
assert.match(policySystemText, /Search host-owned dinner data/);
202+
assert.match(policySystemText, /Surface plan/);
203+
assert.match(policySystemText, /Runtime: `declarative`/);
204+
assert.match(policySystemText, /Data: `host-resource`/);
205+
assert.doesNotMatch(policySystemText, /Rules for scripts/);
206+
207+
const policyLines = policyBody
208+
.trim()
209+
.split(/\n/)
210+
.filter(Boolean)
211+
.map((raw) => JSON.parse(raw) as ProtocolLine);
212+
assert.deepEqual(policyLines.slice(0, 4).map((line) => `${line.op} ${line.path}`), [
213+
'meta /surface-policy',
214+
'meta /surface-plan',
215+
'meta /status',
216+
'set /screen',
217+
]);
218+
assert.equal(policyLines.some((line) => line.path === '/mode-upgraded'), false);
219+
assert.deepEqual((policyLines[0] as Extract<ProtocolLine, { op: 'meta' }>).value, {
220+
tier: 'declarative',
221+
purpose: 'explore',
222+
grants: ['search'],
223+
components: [],
224+
persistence: 'replayable',
225+
});
226+
assert.deepEqual((policyLines[1] as Extract<ProtocolLine, { op: 'meta' }>).value, surfacePlan);
174227
});
175228

176229
function sse(event: string, data: unknown): string {

apps/server/src/main.ts

Lines changed: 63 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import express from 'express';
22
import cors from 'cors';
33
import Anthropic from '@anthropic-ai/sdk';
44
import {
5-
type CapabilityPack,
6-
type ScriptPolicy,
7-
} from '@anarchitecture/summon';
8-
import {
5+
compileSurfacePolicy,
96
parseTokenValues,
7+
type CapabilityPack,
108
type ContractPromptBlock,
119
type ProtocolLine,
10+
type ScriptPolicy,
1211
type SummonLayout,
12+
type SurfacePlan,
1313
type TokenOverride,
1414
} from '@anarchitecture/summon/engine';
1515
import {
@@ -516,6 +516,8 @@ app.post('/api/generate', async (req, res) => {
516516
const edit = parsedEdit.edit;
517517
const repairOptions = parseRepairOptions(req.body?.repair);
518518

519+
const hasSurfacePolicy =
520+
req.body?.surfacePolicy !== undefined && req.body.surfacePolicy !== null;
519521
const requestedMode: 'static' | 'interactive' =
520522
req.body?.mode === 'interactive' ? 'interactive' : 'static';
521523
let scriptPolicy: ScriptPolicy | undefined =
@@ -527,6 +529,7 @@ app.post('/api/generate', async (req, res) => {
527529
let pack: CapabilityPack | null = null;
528530
let modeUpgraded = false;
529531
let inferenceUsed = false;
532+
let surfacePlan: SurfacePlan;
530533

531534
// Shape classification — picks ONE response shape so the per-direction
532535
// block ships only the matching shape exemplar (atoms always ship). Falls
@@ -539,53 +542,64 @@ app.post('/api/generate', async (req, res) => {
539542
shape = await inferShape(anthropic, prompt);
540543
}
541544

542-
// Layer 3: Haiku-based capability inference. Decides mode + narrows the
543-
// pack to the minimal subset of intents the prompt actually needs. The
544-
// pack is treated as a ceiling — inference can only narrow, never expand.
545-
// Falls through to the Layer 2 regex on timeout or error.
546-
if (process.env.SUMMON_INFER_CAPABILITIES === '1' && capabilityCeiling) {
547-
const inferred = await inferPack(anthropic, prompt, capabilityCeiling);
548-
if (inferred) {
549-
inferenceUsed = true;
550-
if (requestedMode === 'interactive') {
551-
// Respect the user's explicit interactive choice. Haiku may narrow
552-
// the pack, but won't downgrade to static.
553-
mode = 'interactive';
554-
pack = inferred.pack ?? capabilityCeiling;
555-
} else {
556-
mode = inferred.mode;
557-
pack = inferred.pack;
558-
modeUpgraded = mode === 'interactive';
545+
if (hasSurfacePolicy) {
546+
const compiledPolicy = compileSurfacePolicy(req.body.surfacePolicy, {
547+
capabilities: capabilityCeiling,
548+
components: componentPack,
549+
});
550+
mode = compiledPolicy.mode;
551+
scriptPolicy = compiledPolicy.scriptPolicy;
552+
pack = compiledPolicy.capabilities;
553+
surfacePlan = compiledPolicy.surfacePlan;
554+
} else {
555+
// Layer 3: Haiku-based capability inference. Decides mode + narrows the
556+
// pack to the minimal subset of intents the prompt actually needs. The
557+
// pack is treated as a ceiling — inference can only narrow, never expand.
558+
// Falls through to the Layer 2 regex on timeout or error.
559+
if (process.env.SUMMON_INFER_CAPABILITIES === '1' && capabilityCeiling) {
560+
const inferred = await inferPack(anthropic, prompt, capabilityCeiling);
561+
if (inferred) {
562+
inferenceUsed = true;
563+
if (requestedMode === 'interactive') {
564+
// Respect the user's explicit interactive choice. Haiku may narrow
565+
// the pack, but won't downgrade to static.
566+
mode = 'interactive';
567+
pack = inferred.pack ?? capabilityCeiling;
568+
} else {
569+
mode = inferred.mode;
570+
pack = inferred.pack;
571+
modeUpgraded = mode === 'interactive';
572+
}
559573
}
560574
}
561-
}
562575

563-
// Layer 2 regex fallback — runs when inference is disabled, ceiling is
564-
// missing, or Haiku returned null (timeout/parse failure). Only upgrades
565-
// when a ceiling exists; without one there's no Capabilities block to emit.
566-
if (!inferenceUsed) {
567-
if (requestedMode === 'static' && capabilityCeiling && detectsInteractiveIntent(prompt)) {
568-
mode = 'interactive';
569-
modeUpgraded = true;
576+
// Layer 2 regex fallback — runs when inference is disabled, ceiling is
577+
// missing, or Haiku returned null (timeout/parse failure). Only upgrades
578+
// when a ceiling exists; without one there's no Capabilities block to emit.
579+
if (!inferenceUsed) {
580+
if (requestedMode === 'static' && capabilityCeiling && detectsInteractiveIntent(prompt)) {
581+
mode = 'interactive';
582+
modeUpgraded = true;
583+
}
584+
pack = mode === 'interactive' ? capabilityCeiling : null;
570585
}
571-
pack = mode === 'interactive' ? capabilityCeiling : null;
572-
}
573586

574-
const resolvedSurface = resolveSurfaceGenerationPlan({
575-
prompt,
576-
mode,
577-
scriptPolicy,
578-
capabilities: pack,
579-
rawSurfacePlan: req.body?.surfacePlan,
580-
rawSurfaceCeiling: req.body?.surfaceCeiling,
581-
});
582-
if (mode !== resolvedSurface.mode) {
583-
modeUpgraded = mode === 'static' && resolvedSurface.mode === 'interactive' ? true : modeUpgraded;
584-
mode = resolvedSurface.mode;
585-
pack = mode === 'interactive' ? pack ?? capabilityCeiling : null;
587+
const resolvedSurface = resolveSurfaceGenerationPlan({
588+
prompt,
589+
mode,
590+
scriptPolicy,
591+
capabilities: pack,
592+
rawSurfacePlan: req.body?.surfacePlan,
593+
rawSurfaceCeiling: req.body?.surfaceCeiling,
594+
});
595+
if (mode !== resolvedSurface.mode) {
596+
modeUpgraded = mode === 'static' && resolvedSurface.mode === 'interactive' ? true : modeUpgraded;
597+
mode = resolvedSurface.mode;
598+
pack = mode === 'interactive' ? pack ?? capabilityCeiling : null;
599+
}
600+
scriptPolicy = resolvedSurface.scriptPolicy;
601+
surfacePlan = resolvedSurface.surfacePlan;
586602
}
587-
scriptPolicy = resolvedSurface.scriptPolicy;
588-
const surfacePlan = resolvedSurface.surfacePlan;
589603

590604
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
591605
res.setHeader('Cache-Control', 'no-cache, no-transform');
@@ -666,10 +680,11 @@ app.post('/api/generate', async (req, res) => {
666680
ghostPrompt: ghostContext?.prompt ?? null,
667681
layout,
668682
edit,
669-
capabilities: pack,
683+
capabilities: hasSurfacePolicy ? capabilityCeiling : pack,
670684
components: componentPack,
671-
scriptPolicy,
672-
surfacePlan,
685+
surfacePolicy: hasSurfacePolicy ? req.body.surfacePolicy : null,
686+
scriptPolicy: hasSurfacePolicy ? undefined : scriptPolicy,
687+
surfacePlan: hasSurfacePolicy ? null : surfacePlan,
673688
tokenOverrides: overrides.applied,
674689
activeTokensCss: ghostContext?.tokenSource.css ?? direction?.tokensCss ?? null,
675690
preludeLines,

0 commit comments

Comments
 (0)