diff --git a/.devcontainer/features/claude/install.sh b/.devcontainer/features/claude/install.sh index 959477634..88aebca30 100755 --- a/.devcontainer/features/claude/install.sh +++ b/.devcontainer/features/claude/install.sh @@ -1,5 +1,14 @@ #!/bin/bash set -euo pipefail -# Install Claude Code CLI -su - "${_REMOTE_USER}" -c "curl -fsSL https://claude.ai/install.sh | bash" +# Install (or upgrade) the Claude Code CLI via the official installer script. +# Runs in two contexts: as root during the feature build (with _REMOTE_USER +# set) and as the remote user if reused later. Drop to the remote user only +# when invoked as root; otherwise run directly. +INSTALL_CMD='curl -fsSL https://claude.ai/install.sh | bash' + +if [ "$(id -u)" = "0" ] && [ -n "${_REMOTE_USER:-}" ]; then + su - "${_REMOTE_USER}" -c "$INSTALL_CMD" +else + eval "$INSTALL_CMD" +fi diff --git a/.devcontainer/postcreate.sh b/.devcontainer/postcreate.sh index 56ae09337..9f6b638bb 100644 --- a/.devcontainer/postcreate.sh +++ b/.devcontainer/postcreate.sh @@ -134,6 +134,27 @@ if [ -f "$DEVENV" ]; then "${PI_PATHS[@]}" > "$HOME/.pi/agent/models.json" fi + # --- Pi Coding Agent settings (default provider/model) --- + # Without this, pi falls back to its built-in "google" provider and a + # bare model id, which then fails against the ambient CLAUDE_CODE_USE_BEDROCK + # env because Bedrock requires a region-prefixed inference profile id. + # The aws profile pins amazon-bedrock + a us.-prefixed model. Later + # configured profiles override earlier ones (shallow merge). + PI_SETTINGS_FILES=() + for profile in "${CONFIGURED[@]}"; do + if [ -f "$PROFILES_DIR/$profile/pi.settings.json" ]; then + PI_SETTINGS_FILES+=("$profile") + fi + done + + if [ ${#PI_SETTINGS_FILES[@]} -ge 1 ]; then + mkdir -p "$HOME/.pi/agent" + PI_SETTINGS_PATHS=() + for p in "${PI_SETTINGS_FILES[@]}"; do PI_SETTINGS_PATHS+=("$PROFILES_DIR/$p/pi.settings.json"); done + jq -s 'reduce .[] as $s ({}; . * $s)' \ + "${PI_SETTINGS_PATHS[@]}" > "$HOME/.pi/agent/settings.json" + fi + # --- Codex CLI config --- # Codex reads ~/.codex/config.toml. Only the aws profile ships one: it # targets OpenAI GPT-5.5 via Codex's built-in amazon-bedrock provider diff --git a/.devcontainer/profiles/aws/pi.settings.json b/.devcontainer/profiles/aws/pi.settings.json new file mode 100644 index 000000000..d82ce4791 --- /dev/null +++ b/.devcontainer/profiles/aws/pi.settings.json @@ -0,0 +1,4 @@ +{ + "defaultProvider": "amazon-bedrock", + "defaultModel": "us.anthropic.claude-sonnet-5" +} diff --git a/AGENTS.md b/AGENTS.md index 7ffa89afb..d3cf9b357 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,6 +40,13 @@ Core components: - SignalR is used for real-time client/server communication. - Do not assume authentication is built in; deployment may rely on an auth proxy. +## Development +- Simplicity, concision, and readability matter much more than flexibility, abstractions, hypothetical reuse, etc. +- Avoid over-engineering or adding unnecessary features. +- Define clear success criteria. +- If something is unclear or confusing, stop and ask questions. Never assume. +- Edit only what you must. Don't needlessly alter adjacent code. + ## Verification Before finishing: diff --git a/configuration/n8n-bootstrap/scripts/provision-n8n.sh b/configuration/n8n-bootstrap/scripts/provision-n8n.sh index a59028908..2737c0ee5 100755 --- a/configuration/n8n-bootstrap/scripts/provision-n8n.sh +++ b/configuration/n8n-bootstrap/scripts/provision-n8n.sh @@ -145,6 +145,8 @@ if [ -d "$WORKFLOW_DIR" ] && ls "$WORKFLOW_DIR"/*.json 1>/dev/null 2>&1; then const dir = '$WORKFLOW_DIR'; const cookie = '$SESSION'; + // Import inactive to avoid webhook/FK registration issues, then activate + // in a separate pass below once the workflow exists. async function importWorkflow(filePath) { const wf = JSON.parse(fs.readFileSync(filePath, 'utf8')); const name = wf.name || path.basename(filePath, '.json'); @@ -165,7 +167,9 @@ if [ -d "$WORKFLOW_DIR" ] && ls "$WORKFLOW_DIR"/*.json 1>/dev/null 2>&1; then res.on('end', () => { if (res.statusCode === 200) { console.log(' Imported: ' + name); - resolve(); + let created; + try { const d = JSON.parse(body).data; created = { id: d.id, versionId: d.versionId }; } catch (e) {} + resolve(created); } else if (res.statusCode === 409) { console.log(' Exists: ' + name + ' (skipped)'); resolve(); @@ -181,12 +185,51 @@ if [ -d "$WORKFLOW_DIR" ] && ls "$WORKFLOW_DIR"/*.json 1>/dev/null 2>&1; then }); } + function activateWorkflow(id, versionId, name) { + const data = JSON.stringify({ versionId }); + return new Promise((resolve) => { + const req = http.request({ + hostname: process.env.N8N_HOST || 'n8n', port: 5678, + path: '/rest/workflows/' + id + '/activate', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(data), + 'Cookie': cookie + } + }, res => { + let body = ''; + res.on('data', c => body += c); + res.on('end', () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + console.log(' Activated: ' + name); + } else { + console.error(' Activate FAILED: ' + name + ' (' + res.statusCode + ')'); + console.error(' ' + body.substring(0, 200)); + } + resolve(); + }); + }); + req.write(data); + req.end(); + }); + } + (async () => { const files = fs.readdirSync(dir).filter(f => f.endsWith('.json')); + const imported = []; for (const f of files) { - await importWorkflow(path.join(dir, f)); + const created = await importWorkflow(path.join(dir, f)); + if (created && created.id) imported.push({ id: created.id, versionId: created.versionId, name: path.basename(f, '.json') }); } console.log('Done: ' + files.length + ' workflows processed.'); + + if (imported.length) { + console.log('Activating ' + imported.length + ' workflows...'); + for (const wf of imported) { + await activateWorkflow(wf.id, wf.versionId, wf.name); + } + } })().catch(e => { console.error(e.message); process.exit(1); }); " echo "[provisioner] Workflow import complete." diff --git a/docs/new.md b/docs/new.md index 2972a433a..4ff2748ba 100644 --- a/docs/new.md +++ b/docs/new.md @@ -1,5 +1,20 @@ # What's New in GHOSTS 9 +## Version 9.1 + +### Automatic NPC pools and default workflows on execution + +Creating an execution now brings a scenario to life automatically: + +- **NPC pools** — each `UserPool` (role + count) defined on the scenario generates that many execution-scoped NPCs when the execution is created. They are grouped by `Campaign` = scenario name, `Enclave` = run name, and `Team` = pool role. Pool NPCs exist as a population regardless of whether they are assigned to a machine (they may live in "greyspace"). +- **Default n8n workflows** — scenarios are seeded with a default set of animation workflow bindings (Belief, Social Graph, Post to Social Media). When an execution starts, these are scheduled on their cron and fired via n8n. Bindings are editable per scenario, referenced by stable webhook path, and the referenced workflows are activated in n8n automatically if inactive. + +### Database is recreated on upgrade + +> **Breaking:** GHOSTS uses `EnsureCreated()` and does not run EF migrations. The 9.1 schema adds a `scenarioworkflowbindings` table, so **an existing 9.0 database must be dropped and recreated** — it will not be altered in place. There is no automatic migration path. + +To upgrade, delete the `ghosts` database (or its Postgres volume) and restart the API; it recreates the schema and re-seeds on startup. Back up any data you need first. + ## Version 9.0 ### Angular 20 Frontend diff --git a/experimental/rpg/fixtures/scenarios/meridian-hybrid.json b/experimental/rpg/fixtures/scenarios/meridian-hybrid.json new file mode 100644 index 000000000..9468dbbc5 --- /dev/null +++ b/experimental/rpg/fixtures/scenarios/meridian-hybrid.json @@ -0,0 +1,142 @@ +{ + "_comment": "Kriegspiel Mode fixture v1. Hybrid cyber + cognitive scenario. The author supplies PIECES, not a fixed timeline: a kickoff situation, the player's mandate/objectives, OPFOR's objective + playbook (what it COULD do, gated by preconditions), a mutable world, and scheduled triggers (the clock's teeth). What actually happens is composed at runtime by the OPFOR agent + the judge. Fog is 'partial': the player perceives emitted indicators, never ground-truth flags. Meridian Logistics is targeted by Cinder Wolf, who couples a network intrusion with an influence campaign to force a ransom by eroding public and board confidence.", + + "catalog": { + "listed": true, + "sortOrder": 1, + "era": "Modern", + "theater": "Meridian Logistics — Hybrid", + "estimatedMinutes": 20 + }, + + "scenario": { + "id": 10, + "name": "Meridian Under Pressure: Hybrid Intrusion", + "description": "You are the incident commander at Meridian Logistics. An adversary is inside your network AND running an influence campaign to break your board's nerve. Deny them their objective before the clock — and the narrative — runs away from you.", + "situation": "0900. You take the incident commander's seat at Meridian Logistics. Overnight, your SOC flagged anomalous authentication on the finance segment, and your comms team noticed a small but coordinated cluster of social posts alleging Meridian mishandles customer shipping data. Nothing is confirmed. The board meets at end of day and will ask two questions: is our data safe, and is our name safe. You have both to protect, and you cannot yet prove either is under attack.", + + "player": { + "role": "Incident Commander, Meridian Logistics", + "mandate": "Protect Meridian's data (deny the intrusion) and Meridian's reputation (deny the influence campaign). You direct both the SOC and the comms cell. You may investigate, contain, isolate, reset credentials, preserve evidence, brief the board, issue public statements, coordinate with the platform/press, and prebunk or debunk claims.", + "roe": "You may act inside Meridian's own estate freely. External statements must be truthful and coordinated with legal. No offensive action against the adversary's infrastructure ('hack back' is out of bounds)." + }, + + "opfor": { + "name": "Cinder Wolf", + "objective": "Force Meridian to pay by (a) establishing the ability to encrypt the finance file share and (b) convincing the board and public that Meridian has already lost control of customer data. Cyber pressure and narrative pressure reinforce each other.", + "winThreshold": 5, + "playbook": [ + { + "id": "harvest-creds", + "domain": "cyber", + "description": "Phish finance staff and harvest a valid credential to deepen access.", + "preconds": "!creds-reset", + "setFlags": ["has-foothold"], + "progress": 1, + "indicators": ["Impossible-travel sign-in flagged on a finance account", "A finance clerk reports a odd 'password expiry' email"] + }, + { + "id": "spread-lateral", + "domain": "cyber", + "description": "Use the foothold to move laterally toward the file server FS01.", + "preconds": "flag:has-foothold && !host-isolated", + "setFlags": ["near-fileshare"], + "progress": 1, + "indicators": ["SMB authentication spikes from FIN-WS-04 to FS01", "EDR notes lateral tooling on a finance workstation"] + }, + { + "id": "stage-ransom", + "domain": "cyber", + "description": "Stage the encryptor on FS01, ready to detonate — the decisive cyber threat.", + "preconds": "flag:near-fileshare && !host-isolated", + "setFlags": ["ransom-staged"], + "progress": 2, + "indicators": ["Shadow-copy deletion attempts observed on FS01", "A new service was created on the file server overnight"] + }, + { + "id": "seed-narrative", + "domain": "cognitive", + "description": "Amplify the 'Meridian leaks customer data' claim through a coordinated inauthentic cluster.", + "preconds": "!narrative-countered", + "setFlags": ["narrative-seeded"], + "progress": 1, + "indicators": ["#MeridianLeaks appears with unnatural posting velocity", "Several near-identical accounts push the same screenshot"] + }, + { + "id": "leak-lure", + "domain": "cognitive", + "description": "Publish a partial (real or fabricated) 'data sample' to make the leak claim credible.", + "preconds": "flag:narrative-seeded && !narrative-countered", + "setFlags": ["leak-published"], + "progress": 2, + "indicators": ["A paste site hosts a file labelled 'Meridian customer export'", "A reporter emails asking Meridian to confirm a breach"] + } + ] + }, + + "world": { + "assets": ["FIN-WS-04 (finance workstation)", "FS01 (finance file server)", "DC01 (domain controller)", "Meridian corporate comms channels"], + "facts": { + "board_meeting": "End of day; board will ask if data and reputation are secure.", + "soc_status": "Anomalous auth flagged on the finance segment; not yet triaged.", + "comms_status": "A small coordinated cluster is alleging data mishandling." + }, + "flags": [], + "narrativeEnv": { + "platforms": "Public microblog platform + a paste site are the adversary's amplification surface.", + "audience": "Board, customers, trade press." + } + }, + + "triggers": [ + { + "id": "press-deadline", + "when": "clock>=40", + "inject": "A trade reporter sets a 30-minute deadline for comment on the alleged data leak.", + "indicators": ["Press deadline: comment requested on the leak allegation"] + }, + { + "id": "board-checkin", + "when": "clock>=70", + "inject": "The board chair asks for an interim read: are our data and our name safe?", + "indicators": ["Board chair requests an interim assessment"] + } + ], + + "clock": { + "windowMinutes": 90, + "tickMinutes": 12, + "label": "to the board meeting" + }, + + "fog": { + "default": "partial" + }, + + "objectives": [ + { + "id": 1, + "name": "Establish the picture", + "description": "Determine whether the authentication anomaly and the social claims are a real, coordinated attack rather than noise.", + "successCriteria": "Player investigates/correlates the cyber and cognitive indicators before committing to a public position.", + "priority": 2 + }, + { + "id": 2, + "name": "Deny the intrusion", + "description": "Stop the adversary reaching and staging ransomware on the finance file share.", + "successCriteria": "The foothold is cut (credentials reset AND the affected host isolated) before the encryptor is staged on FS01.", + "metWhen": "flag:creds-reset && flag:host-isolated", + "priority": 1 + }, + { + "id": 3, + "name": "Deny the narrative", + "description": "Prevent the influence campaign from convincing the board and public that Meridian has lost control of customer data.", + "successCriteria": "Player counters the narrative truthfully (prebunk/debunk + coordinated statement) before the leak lure lands.", + "metWhen": "flag:narrative-countered", + "priority": 1 + } + ] + } +} diff --git a/experimental/rpg/frontend/src/app/app.html b/experimental/rpg/frontend/src/app/app.html index f22e6079b..5e2423c3e 100644 --- a/experimental/rpg/frontend/src/app/app.html +++ b/experimental/rpg/frontend/src/app/app.html @@ -12,8 +12,8 @@ ░▒▓███▀▒░▓█▒░██▓░ ████▓▒░▒██████▒▒ ▒██▒ ░ ▒██████▒▒

SCENARIO PLAYER // GHOSTS RPG

-

Umpired staff exercises. You hold the decision seat.
- Exercise control plays every other cell — including the adversary.

+

Umpired wargames in the Kriegspiel tradition. You hold the decision seat.
+ An umpire judges your reasoning; a thinking adversary plays against you.

@@ -45,7 +45,6 @@

AVAILABLE SCENARIOS

{{ scenario.description }} {{ scenario.estimatedMinutes }} MIN · - {{ scenario.events }} EVENTS · {{ scenario.objectives }} OBJECTIVES @@ -76,11 +75,10 @@

AVAILABLE SCENARIOS

@for (line of lines(); track $index) { @switch (line.kind) { - @case ('dm') { -
+ @case ('beat') { +
- {{ line.time }} - {{ line.cell }} + {{ line.speaker }}

{{ line.text }}

@@ -88,6 +86,9 @@

AVAILABLE SCENARIOS

@case ('player') {
> {{ line.text }}
} + @case ('ruling') { +
⚖ {{ line.text }}
+ } @case ('notice') {
» {{ line.text }}
} @@ -100,7 +101,10 @@

AVAILABLE SCENARIOS

} } @if (busy()) { -
+
+
+ the umpire and adversary are deliberating… {{ elapsed() }}s +
}
@@ -124,50 +128,34 @@

AVAILABLE SCENARIOS

} - - @if (awaiting() && !complete()) { -
- @if (tasks().length > 1) { -
- {{ tasks().length }} issues on your board — submit orders in any order -
- } - @for (t of tasks(); track t.step; let last = $last) { -
-
- TASK {{ t.step }} - {{ t.time }} -
-
- @for (a of t.actions; track a.label) { - - } - @if (last && canTable()) { - - } -
-
- } -
- } - + @if (!complete()) { -
- > - - + +
+ > + +
+
+ because + +
+
} @@ -175,36 +163,30 @@

AVAILABLE SCENARIOS

@if (hud(); as h) {