Scenario Builder and authoring¶
The Scenario Builder turns an exercise developer's plain-English intent into a validated GHOSTS scenario. It
lives in the frontend at /scenarios/{id}/builder and has two steps. Everything it does with a model, extraction
and the conversation, goes through Amazon Bedrock with the model picked on the Sources step.
- Sources. First the Model and its Effort: one pair per scenario, Sonnet at its default effort to begin with. The model both reads the sources and runs the conversation; the effort level sets how hard it thinks in the conversation. Then text, web pages and files the scenario is built from. Each source is split into chunks the conversation searches and cites, and extraction runs as soon as a source is added, reading its chunks into entities (hosts, segments, people, organizations) and a graph. An entity a person marks Reviewed is taken as the real name. Chunks a run leaves pending, usually because the model was unavailable, can be retried from the Sources step.
- Conversation. An authoring agent interviews the developer, drafts a scenario document against the
schema in
schemas/scenario-document/, validates it, and shows the plan, the decisions ledger and the warnings. Nothing is imported until the developer clicks Import; the server validates the exact bytes again and writes them into the Builder's scenario. Every turn shows the server's own gate report of what the agent called and what validated.
Afterwards the scenario opens in the planner, whose Document tab shows the approved version, what any edit since has changed, and downloads of the document and its exercise plan.
Configuration¶
The Builder calls Amazon Bedrock's Converse API. Credentials come from AWS_ACCESS_KEY_ID and
AWS_SECRET_ACCESS_KEY or the default AWS credential chain, never from configuration and never to the browser.
The ScenarioAuthoring section of appsettings.json:
| Key | Meaning | Default |
|---|---|---|
Region |
The Bedrock region. | us-east-1 |
Model |
The model a scenario uses until one is picked on its Sources step. | Sonnet 5.5 |
Models[] |
The models the Sources step offers, each with Name, Id, Efforts, the effort levels it takes (low, medium, high, xhigh, max on current Anthropic and OpenAI models), and Pricing, US dollars per million input, output, cache-read and cache-write tokens. A model with no Efforts has the Sources step's effort control greyed out; one with no Pricing shows token counts but no cost. |
three Anthropic models and GPT-6.1 Sol, at their providers' list prices |
FallbackModel |
For ten minutes after Model returned HTTP 503, a new session that names no model starts here. A session that has begun keeps its model. Empty: no fallback. |
empty |
MaxOutputTokens |
The output limit of each model call. A full draft needs at least 64,000. | 64000 |
PromptCaching |
Cache points on the system prompt, the tools and the conversation. Turn off for a model that rejects them. | true |
RequestTimeoutSeconds |
Each model call's request timeout. | 300 |
TurnTimeoutMinutes |
A turn's overall limit; the model call it is waiting on is cancelled. | 30 |
ValidatorTimeoutSeconds |
Each validator and dry-run call's own timeout. | 90 |
Extraction reads chunks ScenarioBuilder:ExtractionConcurrency at a time (default 6), one model call per
4,000-character chunk.
Who is who. GHOSTS has no sign-in. The API reads the user from the header an authenticating proxy sets,
Identity:UserHeader (X-Forwarded-User by default); without a proxy everyone is anonymous. A new scenario is
its author's draft, shown to its author alone, and cannot run until its author publishes it, which needs its
document to validate with 0 errors. Everything under a scenario, including its sources, chunks, entities, graph
and authoring sessions, follows the same rule.
ATT&CK data. The agent's technique and group lookups read an index embedded in the API. The Builder's
ATT&CK enrichment needs MITRE's STIX bundle in config/AttackData/; see the README there for where to get it.
What a session costs¶
Under the conversation's text box, the page shows the session's model calls, input tokens (with how many were
read from the prompt cache), output tokens, and an estimated cost. The counts are the provider's own, recorded
for every call. The cost multiplies them by the model's configured Pricing, so it is an estimate at list
prices: Bedrock's rate for a region or a provisioned model may differ. With caching on, a drafting session on
Sonnet 5.5 runs about ten calls, a few dozen uncached input tokens, a quarter million cached, and some fifteen
thousand out, which is roughly twenty cents.
Limits to know¶
- One turn at a time, across instances. A session runs one turn or import at a time through a lease row
(
authoring_session_leases), so two API instances serving the same session cannot run two turns at once. A lease an instance died holding is taken over once it is older thanTurnTimeoutMinutesplus five. The SignalR progress feed still has no backplane: with more than one instance, a page hears only the instance that runs its turn. - Bedrock only. Neither the conversation nor extraction has an Ollama provider.
- Citations open the chunk, and name its place. A
[chunk N]citation opens the chunk's text under the source's name, the PDF page the chunk starts on, and its character offset in the source; the search and read tools give the agent the same, for a reference'slocator. Chunks made before this records nothing, until their source is chunked again. A chunk's page is where it starts: with the 500-character overlap, that can be the page before most of its text. - A dry run leaves id gaps. Validating with
dryRuncreates the scenario and its rows inside a transaction that is always rolled back. Postgres sequences are not transactional, so each dry run uses up an id in every table it touched. Accepted: nothing shows those ids as a count, and resetting a sequence would race a real import. - Compile is held to the import's standard.
POST api/scenarios/{id}/builder/compile, which no screen calls, writes its rows in one transaction, derives the document they make, and keeps them only when it validates with 0 errors; that document is stored beside the rows with origincompile. Compiling again adds nothing it added before. - The plan is the server's. The exercise plan under a reply's Plan button, and the
.plan.mdexport, are rendered by the server from the document; the agent's own summary in the reply is prose.
The acceptance tests on a real model¶
ScenarioAuthoringServiceTests covers the server's rules with a scripted model. The specification's
acceptance tests that need a real model (Payroll Week in two messages, Night Shift and Wire Friday to an
importable document, token records on every call, a draft grounded on a network source, a hostile source) are
in Ghosts.Api.Tests/Live. They are skipped unless pointed at a running API, because they cost money and
depend on the provider:
GHOSTS_LIVE_API=http://localhost:5000 GHOSTS_LIVE_EFFORT=medium dotnet test src/Ghosts.Api.Tests --filter FullyQualifiedName~Live
GHOSTS_LIVE_MODEL picks one of the configured models. A turn may take several minutes. Run them on the model a
deployment will rely on before relying on it.