M1 Phase 1: approved-surface manifest + precedence pinning - #301
Conversation
Adds the generation block that turns a tinkerdown.yaml into a manifest -- the set of sources and actions an LLM may wire up when generating an app -- plus describes: metadata on sources and actions, which carries no runtime behavior and exists so an operator reviewing a generated app reads "the pending PII access requests queue" rather than a table name. Approval is enforced by a precedence tier rather than by forbidding frontmatter declarations (operator decision). A conditional ban would have been confusing and forced per-field tracking of what is required versus forbidden. Instead the documented two-tier rule gains a top tier: 1. manifest-approved (pinned) 2. frontmatter 3. config defaults Projects with no generation block behave exactly as before. Why pinning is necessary: a page's frontmatter can declare its own sources and actions, so a generated page could reference an approved name while defining that name as something the operator never approved -- defeating any check that reasons about names alone, since the name really is in the approved set. Pinning makes an approved name mean one thing regardless of what the page says. Attempts are logged rather than silently dropped, so a generating agent sees why its definition had no effect. Sources and actions resolve through different code paths, so each needed its own fix, and the action side needed two. Actions never had the site-config fallback sources have: a page could only invoke actions declared in its own frontmatter, which left generation.actions inert -- naming a surface no generated page could reach, forcing it to declare those actions itself, which is what approval exists to prevent. The fallback is deliberately limited to approved actions rather than every site action; mirroring sources exactly would have made actions written for schedules or webhooks callable from any page, a privilege expansion arriving as a side effect. Tests cover all tiers for both, and each was verified to fail with the implementation neutralised rather than merely to pass with it. Also documents the three-tier precedence and the generation block in docs/reference/config.md, which stated a two-tier rule this makes incomplete. Verification: GOWORK=off go test ./... green including the root package with all 32 !ci e2e files (821s). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M9pJSPmG6i1D8s6rpEV4h
Defers two items to their actual consumers (operator decision). The Manifest accessor would bundle data the config already exposes directly, so building it now means guessing the shape Phase 2 and 3 want and then keeping a parallel representation in sync; Phase 2's Audit decides whether to define it against a real need or strike it. The demo fixture belongs in Phase 4, which builds the PII console fixtures -- seeding it here means inventing sources Phase 4 would rewrite, and the precedence semantics are already covered by tests using in-memory configs. Records the E2E acceptance criterion as NOT met rather than ticking it. I wrote that criterion myself during the Audit precisely because the runtime demo was missing, so quietly satisfying it would be the failure this plan keeps documenting. What is verified: the unit tests call getEffectiveSource and getPageActions directly -- the real resolution functions, not reimplementations. What is not: that a served page reaches them with a shadowing frontmatter and renders the approved data. Carried to Phase 4 with a note to budget for it, since four Audit probes failed to build a working fixture. Learn records that generation.actions would have been inert, that sources and actions resolve through different paths so any "approved things behave differently" rule must be applied and verified twice, and that I cited the wrong mechanism for the shadowing finding and corrected it mid-phase -- which is the argument for keeping evidence-quality caveats rather than rounding them to "verified". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M9pJSPmG6i1D8s6rpEV4h
Review: M1 Phase 1 — approved-surface manifest + precedence pinningSolid, well-scoped change. The precedence design (manifest-approved > frontmatter > config defaults) is sound, the write-up in the plan doc is unusually thorough about what was and wasn't verified, and both resolution paths ( A few things worth a look: 1. Doc comment overstates enforcement (
|
Review-bot catch on #301. The GenerationConfig doc comment claimed "naming an undefined source is a config error, not a silent skip" and nothing enforced it -- my own tests asserted the opposite. The comment and the tests contradicted each other and both shipped. Made the behavior true rather than softening the comment, because the silent version has a real consequence. Approval is what pins a name against redefinition by page frontmatter, so an approved name with nothing behind it never pins anything: `generation: sources: [requets]` would leave `requests` shadowable. The typo removes a protection while appearing to add one, which is the worst shape a config error can take. ValidateGeneration is wired into Load rather than following the ValidateWebhooks/ValidateOutputs pattern -- those have no callers anywhere in the repo, so copying them would have produced a validator that never runs. Verified by the tests failing for the right reasons rather than by the function merely existing. Also adds a YAML round-trip test, per the bot's third note: every other test for this feature builds Config structs directly in Go, so a mistyped yaml tag would have been invisible to all of them -- the block would parse into zero values and approval would silently do nothing. Fixes the config.md anchor by simplifying the heading rather than encoding the fragile double-hyphen anchor GitHub generates from an em dash. Verification: GOWORK=off go test ./... green including the root package with all 32 !ci e2e files (843s). The validator now runs on every config load in the repo. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M9pJSPmG6i1D8s6rpEV4h
|
All three addressed — the first was a genuine contradiction I shipped, and it was worse than a doc nit. 1. The comment claimed enforcement that didn't exist, and my own tests asserted the opposite. You're right that they disagreed. I made the behavior true rather than softening the comment, because the silent version has a real consequence: approval is what pins a name against redefinition, so an approved name with nothing behind it never pins anything.
Note the resolution-layer tests you spotted ("does not invent a source" / "does not synthesise one") are unchanged and still correct — those assert defense-in-depth at the lookup layer. Config load now rejects the typo earlier; the lookup still refuses to invent anything if one slips through. 2. Anchor — fixed by simplifying the heading to 3. YAML round-trip — added, and you were right that it's a real gap rather than a nicety: every other test builds Full suite green at 843s with the validator now running on every config load in the repo. |
Review: M1 Phase 1 — approved-surface manifest + precedence pinningOverall this is a well-scoped, carefully-documented change. The PR description and plan doc do a lot of the reviewer's work already (rejected alternatives, the Code quality
Bugs / correctness
PerformanceNo concerns — SecurityThis is fundamentally a privilege/precedence fix, and it's treated with the right amount of rigor:
Test coverageStrong for what's in scope: Docs / nit
Nothing here blocks merging — nice work tying the runtime fix, the config validation, and the plan-doc audit trail together. |
Two non-blocking review notes on #301. The IsManifest() half of getEffectiveSource's guard was implied -- ApprovedSource already returns false whenever Generation is nil. Removed there. approvedSiteActions keeps its IsManifest() check because it goes on to dereference Generation.Actions, so the nil guard is load-bearing rather than stylistic. Adds the CHANGELOG entry for generation:, which is a new user-facing config block. Records the three things a reader needs and none of which are obvious from the field list: that approval is a precedence tier rather than a prohibition and why pinning is necessary at all; that approval is also what makes site-level actions reachable, with the fallback deliberately limited so schedule- and webhook-only actions stay unreachable from pages; and that an approved name referring to nothing is rejected at load, because such an entry would be silently inert and a typo would remove a protection while appearing to add one. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M9pJSPmG6i1D8s6rpEV4h
|
Both taken. Redundant guard — removed from CHANGELOG — added under Separately, flagging something I hit while wiring |
Review: M1 Phase 1 — approved-surface manifest + precedence pinningSolid piece of work — the PR description and commit history already do most of the reviewer's job (the self-correction of the shadowing mechanism, the "actions were inert" finding, and the follow-up fixing the Correctness
Test coverage
Design
Minor / non-blocking
Security
Nice, well-documented change with good self-correction visible in the commit history. Approving from a code standpoint; the acknowledged E2E gap is reasonably deferred to Phase 4 per the plan doc. |
First phase of M1 in the ephemeral-UI reframe plan, following M0 (#296, #299, #300).
Adds the
generation:block that turns atinkerdown.yamlinto a manifest — the set of sources and actions an LLM may wire up when generating an app — plusdescribes:metadata carrying no runtime behavior, so an operator reviewing a generated app reads "the pending PII access requests queue" rather than a table name.Approval is a precedence tier, not a prohibition
The rejected alternative was "when a manifest is present, frontmatter may not declare
sources:/actions:at all." A conditional ban is confusing and forces per-field tracking of what's required versus forbidden. Instead the documented two-tier rule gains a top tier:Projects with no
generation:block behave exactly as before.Why pinning is necessary: a page's frontmatter can declare its own sources and actions. Without it, a generated page could reference an approved name while defining that name as something the operator never approved — defeating any check that reasons about names alone, since the name really is in the approved set. Pinning makes an approved name mean one thing regardless of what the page says. Attempts are logged, not silently dropped, so a generating agent sees why its definition had no effect.
The action side needed two changes, and one was a missing capability
Sources and actions resolve through different code paths, so each needed its own fix — and actions needed more than a guard:
generation.actionsinert — naming a surface no generated page could reach, forcing it to declare those actions itself, which is precisely what approval exists to prevent. Since all the privilege lives in actions (scoped export, approve, deny, audit append), the manifest would have governed only the harmless half of the system.The fallback is deliberately limited to approved actions. Mirroring sources exactly would have made every action in
tinkerdown.yaml— including ones written for schedules or webhooks — callable from any page: a privilege expansion arriving as a side effect of a manifest feature. A test pins that projects without a generation block see no such leak.Verification
TestGetEffectiveSourcePrecedence(6 cases) andTestGetPageActionsPrecedence(5 cases) cover both resolution pathsGOWORK=off go test ./...green including the root package with all 32//go:build !cie2e files (821s)One acceptance criterion is recorded as NOT met. I wrote an E2E criterion during the Audit precisely because a runtime demo was missing, so quietly ticking it would be the failure this plan keeps documenting. The unit tests call
getEffectiveSource/getPageActionsdirectly — the real resolution functions, not reimplementations — so the logic is proven where it runs. Not proven: that a served page reaches them with a shadowing frontmatter and renders approved data. Carried to Phase 4, which builds the fixture it needs.Deferrals (operator decision)
Manifestaccessor → Phase 2. The config already exposes approved sources, actions and style guide directly; building the struct now means guessing a shape and maintaining a parallel representation. Phase 2 defines it against a real consumer or strikes it.Also recorded for Phase 4
Action.Statementis a single statement — nothing splits on;. Phase 4 specifies Approve as "runs the scoped export and appends an audit record": two operations. Split into two actions, an approve can succeed while its audit append fails, destroying the durable-audit guarantee that is the demo's justification. Flagged as a deliberate Phase 4 decision rather than something to discover mid-wiring.🤖 Generated with Claude Code
https://claude.ai/code/session_018M9pJSPmG6i1D8s6rpEV4h