Skip to content
SarvexDocs
Esc
↑↓navigate↵open⌘Jpreview
On this page

Add an agent surface

A surface is a place agents reach a store from beyond the store's own UCP profile — a partner marketplace or an agent platform's own channel, say; direct_agent is the bundled, zero-config one. A…

Generated from docs/HOWTO-add-surface.md in the Sarvex repository — edit the source there, not this page.

A surface is a place agents reach a store from beyond the store’s own UCP profile — a partner marketplace or an agent platform’s own channel, say; direct_agent is the bundled, zero-config one. A surface is declared, not coded: nothing in the enrollment state machine, the API layer, activation, the sync runner, the serving route, or the console’s page/store/enrollment component changes. You add modules and one registration line per plane.

The rule is enforced: internal/archtest TestSurfaceBoundaries fails if a surface id or surface module is named anywhere but its own module, the shared id package, or a registration root.

1. Name it once

internal/pkg/ucp/surfaces/surfaces.go: add the id const, add it to All, and any names its two planes must agree on (artifact id, config keys) — this is the only shared file, and it is data.

2. Control half — internal/planes/control/surface/<name>/

Write a domain.SurfaceDescriptor and a surface.Registration:

Descriptor field Meaning
PrerequisiteChecks named surface.PrerequisiteChecks, in the order the merchant sees them
RequiredConnectors / RequiredCapabilities enforced generically as prerequisite rows
ConfigSchema typed fields (text, bool, string_list, json) → the console draws the form; SetConfig validates against it. Omit for free-form config
Artifacts what Sarvex generates for the store
ExternalSteps what the merchant does elsewhere (owner merchant or surface)
VerificationSignal what proves it worked
AutoEnroll enrolls itself when the store is active (zero-config surfaces only)

If the artifact is generated by the sync half, don’t write a reader: use surface.NewSyncedArtifact, surface.NewSyncedSignal and surface.NewArtifactExistsCheck over surface.SyncedArtifactReader.

Register it: one line in surface/all/all.go.

3. Sync half (only if Sarvex generates an artifact) — internal/planes/sync/surface/<name>/

Implement surface.Generator (SurfaceID, ArtifactID, Extension, Generate(ctx, Input) (Output, error)) — deterministic, side-effect free; its Input carries the merchant’s surface config and the store’s products. Register it: one line in sync/surface/all/all.go. The runner does the rest: gates on enrollment, mints a stable token, writes the cache and sync_surface_artifacts, debounces catalog changes, unpublishes on withdrawal, attributes fetches, and emits sync.surface_artifact.updated (which makes the control plane re-evaluate).

If the surface’s attributes or scoring rules differ from core, tag them with the surface id in the catalogue (AttributeField.Surface, ScoringRule.Surface); the sync plane already scopes resolve/score to the store’s surfaces.

4. Console — web/console/src/surfaces/<surface_id>/locale.en.ts

The folder name equals the surface id; it exports the surface’s words, merged under surfaces.<id>. Needed keys: name, description, and whatever the descriptor references (prereq.*, remediation.*, config.<field>.label|help, step.*, artifact.*), plus check.<check_id> (pre-enrollment preview), artifactDetail.<key> and signal.<signal>. A missing key degrades to a humanised id, never a raw dotted key. The page, store, enrollment flow and config form are generic. SurfacesSurface.test.ts lists the keys of the demo example_marketplace surface; add the same for yours.

For the standalone (mock) console, add the surface to src/api/surfaceMock.ts; the example_marketplace entry there is a complete worked example (artifact, prerequisites, config fields, external steps, verification signal) that exists only in mock mode.

5. Check it

  • control: add an end-to-end test next to surface/extensibility_test.go, which enrolls a fixture surface end to end against the public registry API (enroll → config → artifact → submit → signal → enrolled; tenant fence; direct_agent undisturbed)
  • sync: a generator test (deterministic output, config handling)
  • go test ./internal/archtest/ ./internal/planes/... and cd web/console && npm run typecheck && npm run test:run
  • If the surface changes the OpenAPI (new descriptor field), npm run gen:api.

Was this page helpful?