chore: bootstrap lean sysadmin-chronicles repo
Import the runnable game code, content, docs, scripts, and repo guidance while leaving local agent state, dependency installs, build output, and backup copies out of the published tree.
@@ -0,0 +1,42 @@
|
||||
|
||||
# Local agent/runtime state
|
||||
.agents/
|
||||
.claude/
|
||||
.claude-flow/
|
||||
.codex/
|
||||
.swarm/
|
||||
.agent-backups/
|
||||
.agent-logs/
|
||||
.agent-prompts/
|
||||
|
||||
# Build outputs and dependencies
|
||||
node_modules/
|
||||
dist/
|
||||
coverage/
|
||||
.vite/
|
||||
.svelte-kit/
|
||||
frontend/node_modules/
|
||||
server/node_modules/
|
||||
frontend/dist/
|
||||
|
||||
# Local data and cache files
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite-shm
|
||||
*.sqlite-wal
|
||||
*.tar.gz
|
||||
*.log
|
||||
originals-*/
|
||||
.cache/
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Common editor / OS cruft
|
||||
.DS_Store
|
||||
*.swp
|
||||
*~
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"ruflo": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"ruflo@latest",
|
||||
"mcp",
|
||||
"start"
|
||||
],
|
||||
"autoStart": false
|
||||
},
|
||||
"claude-flow": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@claude-flow/cli@latest",
|
||||
"mcp",
|
||||
"start"
|
||||
],
|
||||
"env": {
|
||||
"npm_config_update_notifier": "false",
|
||||
"CLAUDE_FLOW_MODE": "v3",
|
||||
"CLAUDE_FLOW_HOOKS_ENABLED": "true",
|
||||
"CLAUDE_FLOW_TOPOLOGY": "hierarchical-mesh",
|
||||
"CLAUDE_FLOW_MAX_AGENTS": "15",
|
||||
"CLAUDE_FLOW_MEMORY_BACKEND": "hybrid"
|
||||
},
|
||||
"autoStart": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
# sysadmin-chronicles
|
||||
|
||||
@RTK.md
|
||||
|
||||
> Multi-agent orchestration framework for agentic coding
|
||||
|
||||
## Project Overview
|
||||
|
||||
A Claude Flow powered project
|
||||
|
||||
**Tech Stack**: TypeScript, Node.js
|
||||
**Architecture**: Domain-Driven Design with bounded contexts
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Dual-Agent Note
|
||||
|
||||
- This repo is set up for both Claude and Codex CLI
|
||||
- If shell-based delegation is needed, prefer `/home/aaron/.npm-global/bin/codex` instead of relying on `codex` being on `PATH`
|
||||
- Use RuFlo for orchestration and Codex for execution when a second coding agent is helpful
|
||||
- Use `rtk` for noisy shell commands to reduce token/context usage; see `RTK.md`
|
||||
|
||||
## Project Map
|
||||
|
||||
Before substantive work, read `PROJECT_MAP.md`.
|
||||
|
||||
Use it to:
|
||||
- identify relevant files before loading context
|
||||
- follow hot paths and change-impact notes
|
||||
- avoid known anti-patterns
|
||||
|
||||
Update `PROJECT_MAP.md` when:
|
||||
- commands, routes, public APIs, or user-facing workflows change
|
||||
- meaningful files/modules are added, removed, or renamed
|
||||
- persistence formats or compatibility contracts change
|
||||
- feature ownership or architecture changes
|
||||
- major known issues are discovered or resolved
|
||||
|
||||
Do not update it for tiny refactors, wording tweaks, or dependency bumps that do not change workflow or structure.
|
||||
|
||||
### Build
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Test
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
### Development
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Agent Coordination
|
||||
|
||||
### Swarm Configuration
|
||||
|
||||
This project uses hierarchical swarm coordination for complex tasks:
|
||||
|
||||
| Setting | Value | Purpose |
|
||||
|---------|-------|---------|
|
||||
| Topology | `hierarchical` | Queen-led coordination (anti-drift) |
|
||||
| Max Agents | 8 | Optimal team size |
|
||||
| Strategy | `specialized` | Clear role boundaries |
|
||||
| Consensus | `raft` | Leader-based consistency |
|
||||
|
||||
### When to Use Swarms
|
||||
|
||||
**Invoke swarm for:**
|
||||
- Multi-file changes (3+ files)
|
||||
- New feature implementation
|
||||
- Cross-module refactoring
|
||||
- API changes with tests
|
||||
- Security-related changes
|
||||
- Performance optimization
|
||||
|
||||
**Skip swarm for:**
|
||||
- Single file edits
|
||||
- Simple bug fixes (1-2 lines)
|
||||
- Documentation updates
|
||||
- Configuration changes
|
||||
|
||||
### Available Skills
|
||||
|
||||
Use `$skill-name` syntax to invoke:
|
||||
|
||||
| Skill | Use Case |
|
||||
|-------|----------|
|
||||
| `$swarm-orchestration` | Multi-agent task coordination |
|
||||
| `$memory-management` | Pattern storage and retrieval |
|
||||
| `$sparc-methodology` | Structured development workflow |
|
||||
| `$security-audit` | Security scanning and CVE detection |
|
||||
|
||||
### Agent Types
|
||||
|
||||
| Type | Role | Use Case |
|
||||
|------|------|----------|
|
||||
| `researcher` | Requirements analysis | Understanding scope |
|
||||
| `architect` | System design | Planning structure |
|
||||
| `coder` | Implementation | Writing code |
|
||||
| `tester` | Test creation | Quality assurance |
|
||||
| `reviewer` | Code review | Security and quality |
|
||||
|
||||
## Code Standards
|
||||
|
||||
### File Organization
|
||||
- **NEVER** save to root folder
|
||||
- `/src` - Source code files
|
||||
- `/tests` - Test files
|
||||
- `/docs` - Documentation
|
||||
- `/config` - Configuration files
|
||||
|
||||
### Quality Rules
|
||||
- Files under 500 lines
|
||||
- No hardcoded secrets
|
||||
- Input validation at boundaries
|
||||
- Typed interfaces for public APIs
|
||||
- TDD London School (mock-first) preferred
|
||||
|
||||
### Commit Messages
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
Co-Authored-By: claude-flow <ruv@ruv.net>
|
||||
```
|
||||
|
||||
Types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`
|
||||
|
||||
## Security
|
||||
|
||||
### Critical Rules
|
||||
- NEVER commit secrets, credentials, or .env files
|
||||
- NEVER hardcode API keys
|
||||
- Always validate user input
|
||||
- Use parameterized queries for SQL
|
||||
- Sanitize output to prevent XSS
|
||||
|
||||
### Path Security
|
||||
- Validate all file paths
|
||||
- Prevent directory traversal (../)
|
||||
- Use absolute paths internally
|
||||
|
||||
## Memory System
|
||||
|
||||
### Storing Patterns
|
||||
```bash
|
||||
npx @claude-flow/cli memory store \
|
||||
--key "pattern-name" \
|
||||
--value "pattern description" \
|
||||
--namespace patterns
|
||||
```
|
||||
|
||||
### Searching Memory
|
||||
```bash
|
||||
npx @claude-flow/cli memory search \
|
||||
--query "search terms" \
|
||||
--namespace patterns
|
||||
```
|
||||
|
||||
## Links
|
||||
|
||||
- Documentation: https://github.com/ruvnet/claude-flow
|
||||
- Issues: https://github.com/ruvnet/claude-flow/issues
|
||||
@@ -0,0 +1,190 @@
|
||||
# SYSADMIN CHRONICLES — AGENT WORKING RULES
|
||||
> Version 2.0 | Status: Enforced
|
||||
>
|
||||
> Changelog:
|
||||
> v2.0 — Rewritten for Node.js + Svelte era. GDScript/Godot rules removed.
|
||||
> v1.1 — GDScript-specific rules (superseded).
|
||||
>
|
||||
> Read this file FIRST before touching anything else. These rules prevent content
|
||||
> corruption, broken cross-references, and silent design drift.
|
||||
|
||||
---
|
||||
|
||||
## 0. ALWAYS READ FIRST
|
||||
|
||||
Before doing any work, read these files in order:
|
||||
1. `AGENT_RULES.md` (this file)
|
||||
2. `OPEN_ISSUES.md` — current known issues and decisions in progress
|
||||
3. `docs/ARCHITECTURE.md` — system design and constraints
|
||||
4. `docs/QUEST_AUTHORING.md` — content schema and validation rules
|
||||
|
||||
If you are working on a specific domain, also read:
|
||||
- Content work → `docs/QUEST_AUTHORING.md` + relevant `content/world_flags/world_flags.json`
|
||||
- VM work → `docs/ARCHITECTURE.md` sections 5 and 6
|
||||
- Save system work → `docs/SAVE_SYSTEM.md`
|
||||
- Server work → `server/src/` — read the relevant service file before editing
|
||||
|
||||
---
|
||||
|
||||
## 1. WHAT YOU MAY DO WITHOUT ASKING
|
||||
|
||||
- Add new `.js` or `.svelte` files in `server/src/` or `frontend/src/` following existing conventions
|
||||
- Add new JSON content files in `content/` that pass content validation
|
||||
- Add new shell scripts in `tools/` that do not modify VM state
|
||||
- Edit files you created in the current working session
|
||||
- Run read-only commands: `cat`, `ls`, `grep`, `diff`, `virsh domstate`, probes
|
||||
- Run content validation: `node tools/content/validate-content.js`
|
||||
- Run server tests: `cd server && npm test`
|
||||
- Create new files in `tools/vm/quest-prep/` for new quests
|
||||
|
||||
## 2. WHAT YOU MUST ASK BEFORE DOING
|
||||
|
||||
- Modifying `docs/ARCHITECTURE.md`, `docs/QUEST_AUTHORING.md`, `docs/SAVE_SYSTEM.md`, or `docs/ROADMAP.md`
|
||||
- Modifying `content/world_flags/world_flags.json`
|
||||
- Modifying any existing quest, ticket, incident, or dialogue JSON file
|
||||
- Adding a new Express route or WebSocket event type
|
||||
- Changing any validation rule type name or schema field name
|
||||
- Changing VM profile IDs, snapshot names, or network profile names
|
||||
- Any `virsh` command that modifies state: `start`, `destroy`, `snapshot-create`, `snapshot-revert`
|
||||
- Any `tools/vm/` script that writes to a VM image
|
||||
|
||||
## 3. WHAT YOU MUST NEVER DO
|
||||
|
||||
- Delete any file (use a rename to `.bak` and ask first)
|
||||
- Run `virsh undefine`, `virsh pool-delete`, or `virsh net-destroy` without explicit instruction
|
||||
- Run `tools/vm/snapshot-all.sh --revert-to` without explicit instruction
|
||||
- Modify a VM's baseline snapshot or `baseline.clean` state
|
||||
- Run provisioning scripts (`Q0XX-prep.sh`) against any VM without explicit instruction
|
||||
- Add a world flag reference in any content file without first adding it to `world_flags.json`
|
||||
- Create a solution branch with a `priority` that duplicates an existing branch in the same quest
|
||||
- Set `follow_up_incident` to an incident ID that does not exist as a file
|
||||
- Set `series_id` in a dialogue file without ensuring at least 2 members share that series_id
|
||||
- Modify the save file schema without updating `server/src/services/SaveState.js` and the migration handler
|
||||
- Ignore content validation errors and proceed anyway
|
||||
|
||||
---
|
||||
|
||||
## 4. CONTENT AUTHORING RULES
|
||||
|
||||
### World Flags
|
||||
- Every flag used anywhere must exist in `content/world_flags/world_flags.json`
|
||||
- When you set a flag in a quest or incident, update `set_by` in the registry
|
||||
- When you read a flag in a quest, incident, or dialogue, update `read_by`
|
||||
- Conflicting flags must list each other in `conflicts_with`
|
||||
- A flag with `persists: false` resets at the start of each new shift (not on load)
|
||||
|
||||
### Quests
|
||||
- Every quest must have a `clue_fingerprint` with at least one evidence entry
|
||||
- Every quest must declare `required_vms` — list ALL VMs touched, not just the primary
|
||||
- Branch priorities must be unique within a quest — no two branches share a priority number
|
||||
- The highest-priority branch that matches wins — author branches so better fixes have higher priority
|
||||
- Do not author a branch that cannot be distinguished from another branch by validation rules alone
|
||||
|
||||
### Tickets
|
||||
- Both `initial_priority` and `current_priority` must be present and equal at authoring time
|
||||
- `current_priority` is the only field the runtime modifies — never change `initial_priority` at runtime
|
||||
|
||||
### Incidents
|
||||
- Every incident must declare `blast_radius_quests` (can be empty array, never omit)
|
||||
- Every incident must declare `blast_radius_incidents` (can be empty array, never omit)
|
||||
- `follow_up_incident` in a quest branch must map to an incident file that exists
|
||||
|
||||
### Dialogue
|
||||
- If `series_id` is declared, `series_position` must also be declared
|
||||
- A `series_id` must have at least 2 dialogue files sharing it before content passes validation
|
||||
- `trigger: "world_flag:{id}"` — the flag ID must exist in the registry
|
||||
|
||||
### File Naming
|
||||
- Quest files: `Q{NNN}-{kebab-case-title}.json`
|
||||
- Ticket files: `T{NNN}.json`
|
||||
- Incident files: `I{NNN}-{kebab-case-title}.json`
|
||||
- Dialogue files: `{character}-Q{NNN}.json` or `{character}-Q{NNN}-{variant}.json`
|
||||
- Do NOT bundle multiple dialogue characters or quests into one file
|
||||
- VM profiles: `{snake_case}.json`
|
||||
- Quest prep scripts: `Q{NNN}-prep.sh`
|
||||
|
||||
---
|
||||
|
||||
## 5. CODE AUTHORING RULES
|
||||
|
||||
### Node.js (server/src/)
|
||||
|
||||
- All host commands go through `server/src/lib/ssh.js` or `server/src/lib/virsh.js` — never use `child_process` directly in service files
|
||||
- All VM lifecycle actions go through `VMManager.js` — never call libvirt directly from quest or validation logic
|
||||
- Never hardcode VM domain names — use constants from `ContentLoader` or the VM profile JSON
|
||||
- All world flag reads and writes go through `QuestEngine.js` — never mutate flags directly
|
||||
- Trust changes go through `TrustSystem.js` — never modify trust score directly
|
||||
- Services coordinate via `eventBus.js` (Node EventEmitter) — no service may `require()` another service and call its internals directly; emit events instead
|
||||
- All save-state writes go through `SaveState.js`
|
||||
|
||||
### Svelte (frontend/src/)
|
||||
|
||||
- All API calls go through `frontend/src/lib/api.js` — no raw `fetch()` in components
|
||||
- WebSocket events are received in `App.svelte` and distributed to panels via Svelte stores or props — panels do not open their own WebSocket connections
|
||||
- No game logic in Svelte components — components render state and dispatch user actions only
|
||||
|
||||
### Validation Rules
|
||||
|
||||
- Every new rule type must be added to `server/src/services/ValidationEngine.js` and the QUEST_AUTHORING.md rule reference table
|
||||
- Rules must only observe state — they must never modify VM state
|
||||
|
||||
### Shell Scripts
|
||||
|
||||
- All scripts in `tools/vm/` must print a dry-run summary before modifying anything
|
||||
- All scripts must be idempotent — running them twice must produce the same result
|
||||
- Scripts that require root must check for permissions and exit clearly if absent
|
||||
- Use `sc-` prefix for all libvirt resources created by the game
|
||||
|
||||
---
|
||||
|
||||
## 6. VM SAFETY RULES
|
||||
|
||||
- Never operate on a VM domain that does not start with `sc-`
|
||||
- Never revert a snapshot during an active quest without explicit player/developer instruction
|
||||
- The workstation VM (`sc-workstation`) must stay live during all gameplay — never suspend it mid-session
|
||||
- If a probe or validation script fails, log the failure and return a degraded-state result — never crash the server
|
||||
- All SSH connections from the host to guests use key-based auth only — no passwords in scripts
|
||||
|
||||
---
|
||||
|
||||
## 7. HOW TO HANDLE AMBIGUITY
|
||||
|
||||
If you are unsure whether something is correct:
|
||||
1. Check `OPEN_ISSUES.md` — the answer may already be there
|
||||
2. Check `docs/QUEST_AUTHORING.md` for schema rules
|
||||
3. Check `content/world_flags/world_flags.json` for flag semantics
|
||||
4. If still unsure, **stop and ask** rather than making an assumption
|
||||
|
||||
Do not proceed with a best-guess implementation of something that is in
|
||||
`OPEN_ISSUES.md` as unresolved. Wait for a decision.
|
||||
|
||||
---
|
||||
|
||||
## 8. AFTER MAKING CHANGES
|
||||
|
||||
After any content change:
|
||||
- Run `node tools/content/validate-content.js` and confirm zero errors
|
||||
- If you added a world flag, confirm it appears in `world_flags.json` with correct `set_by` and `read_by`
|
||||
- If you added a quest, confirm its prep script exists or is noted as pending in `OPEN_ISSUES.md`
|
||||
|
||||
After any server code change:
|
||||
- Run `cd server && npm test` and confirm no regressions
|
||||
- If you added a new validation rule type, add it to `docs/QUEST_AUTHORING.md`
|
||||
|
||||
After any architectural change (new route, new VM, new service, new WebSocket event):
|
||||
- Update `docs/PROJECT_MAP.md` — boot flow, service graph, VM identity table, or known gaps as applicable
|
||||
|
||||
---
|
||||
|
||||
## 9. DO NOT SILENTLY FIX DESIGN ISSUES
|
||||
|
||||
If you discover a design inconsistency (e.g., two quests that conflict, a flag used
|
||||
incorrectly, a branch that cannot be validated), do NOT silently patch it.
|
||||
|
||||
Instead:
|
||||
1. Add it to `OPEN_ISSUES.md` with a clear description
|
||||
2. Flag it in your response to the developer
|
||||
3. Wait for a decision before changing any content
|
||||
|
||||
The exception is purely mechanical errors (typos, missing commas, wrong field
|
||||
names) where the intent is unambiguous — those can be fixed directly.
|
||||
@@ -0,0 +1,188 @@
|
||||
# Claude Code Configuration — Ruflo Dual-Agent Workflow
|
||||
|
||||
## Agent Roles (Non-Negotiable)
|
||||
|
||||
| Agent | Responsibilities |
|
||||
|-------|-----------------|
|
||||
| **Claude** | Planning, architecture, pseudocode, tradeoff analysis, validation, review |
|
||||
| **Codex** | All implementation: writing files, editing code, CLI ops, refactoring, debugging |
|
||||
| **Ruflo** | Orchestration, task delegation, shared memory between agents |
|
||||
|
||||
**Claude MUST NOT write production code, full implementations, or complete file edits.**
|
||||
**Claude MUST NOT output full files or multi-function implementations — all such work must be delegated.**
|
||||
**Claude MUST delegate all execution to Codex via Ruflo before attempting it directly.**
|
||||
|
||||
## The Mandatory Workflow Loop
|
||||
|
||||
For every task that involves implementation:
|
||||
|
||||
1. **Analyze** — understand requirements, constraints, relevant files
|
||||
2. **Decompose** — break into discrete units (each unit = one Codex call)
|
||||
3. **Delegate** — assign each unit to Codex with a clear, structured spec
|
||||
4. **Wait** — do not add tool calls after delegating; wait for results
|
||||
5. **Review** — critically examine Codex output for correctness and completeness
|
||||
6. **Refine** — if output is wrong or incomplete, re-delegate with corrected spec
|
||||
|
||||
**FAILSAFE: If Claude detects it is about to write code, edit a file, or run a CLI command — STOP and delegate instead.**
|
||||
|
||||
## Delegation Rules
|
||||
|
||||
**Use Codex for:**
|
||||
- Writing any source file (any language)
|
||||
- Editing existing files
|
||||
- Refactoring and mechanical fixes
|
||||
- Running validation, lint, tests
|
||||
- Writing shell scripts and config files
|
||||
- Debugging implementation errors
|
||||
|
||||
**Use Claude for:**
|
||||
- Requirements analysis
|
||||
- System design and API contract design
|
||||
- Pseudocode and algorithm sketches (illustrative only, not production)
|
||||
- Architecture Decision Records
|
||||
- Reviewing and critiquing Codex output
|
||||
- Tradeoff analysis
|
||||
|
||||
**Codex invocation pattern:**
|
||||
```bash
|
||||
/home/aaron/.npm-global/bin/codex "<structured task spec with exact file paths, requirements, and acceptance criteria>"
|
||||
```
|
||||
|
||||
## Exceptions — Claude Writes Directly Only When
|
||||
|
||||
- The task requires judgment Codex demonstrably cannot provide (novel validation logic, cross-reference reasoning)
|
||||
- Codex has already failed on the same task in this session
|
||||
- The change is a single Edit tool call on a non-production file (config, doc)
|
||||
|
||||
## Behavioral Rules (Always Enforced)
|
||||
|
||||
- Read `CLAUDE.md`, `AGENTS.md`, and `AGENT_RULES.md` before starting substantive work
|
||||
- Read `RTK.md` and prefer `rtk` for noisy shell output, including Codex task specs that ask agents to inspect, search, test, or summarize command output
|
||||
- Do what has been asked; nothing more, nothing less
|
||||
- NEVER create files unless absolutely necessary for achieving the goal
|
||||
- ALWAYS prefer editing an existing file to creating a new one
|
||||
- NEVER proactively create documentation or README files unless explicitly requested
|
||||
- NEVER save working files or scratch notes to the root folder
|
||||
- ALWAYS read a file before editing it
|
||||
- NEVER commit secrets, credentials, or `.env` files
|
||||
- Never continuously check status after spawning a swarm — wait for results
|
||||
|
||||
## File Organization
|
||||
|
||||
- `/src` — source code
|
||||
- `/tests` — test files
|
||||
- `/docs` — documentation and markdown
|
||||
- `/config` — configuration files
|
||||
- `/scripts` — utility scripts
|
||||
- `/examples` — example code
|
||||
|
||||
## Project Architecture
|
||||
|
||||
- Domain-Driven Design with bounded contexts
|
||||
- Files under 500 lines
|
||||
- Typed interfaces for all public APIs
|
||||
- TDD London School (mock-first) for new code
|
||||
- Event sourcing for state changes
|
||||
- Input validation at all system boundaries
|
||||
|
||||
### Project Config
|
||||
|
||||
- **Topology**: hierarchical-mesh
|
||||
- **Max Agents**: 15
|
||||
- **Memory**: hybrid
|
||||
- **HNSW**: Enabled
|
||||
- **Neural**: Enabled
|
||||
|
||||
## Build & Test
|
||||
|
||||
```bash
|
||||
npm run build # Build
|
||||
npm test # Test
|
||||
npm run lint # Lint
|
||||
```
|
||||
|
||||
- ALWAYS run tests after any code change (via Codex)
|
||||
- ALWAYS verify build succeeds before committing
|
||||
|
||||
## Security Rules
|
||||
|
||||
- NEVER hardcode API keys, secrets, or credentials in source files
|
||||
- NEVER commit `.env` files or any file containing secrets
|
||||
- Always validate user input at system boundaries
|
||||
- Always sanitize file paths to prevent directory traversal
|
||||
- Run `npx @claude-flow/cli@latest security scan` after security-related changes
|
||||
|
||||
## Concurrency: 1 Message = All Related Operations
|
||||
|
||||
- All operations MUST be concurrent/parallel in a single message
|
||||
- ALWAYS spawn ALL agents in ONE message with full instructions via Agent tool
|
||||
- ALWAYS batch ALL file reads in ONE message
|
||||
- ALWAYS batch ALL Bash commands in ONE message
|
||||
|
||||
## Swarm Orchestration
|
||||
|
||||
- Initialize swarm via CLI for complex tasks before delegating
|
||||
- Spawn concurrent agents using Claude Code's Agent tool
|
||||
- Never use CLI tools alone for execution — Agent tool agents do the actual work
|
||||
- Call CLI tools AND Agent tool in ONE message for complex work
|
||||
|
||||
```bash
|
||||
npx @claude-flow/cli@latest swarm init --topology hierarchical --max-agents 8 --strategy specialized
|
||||
```
|
||||
|
||||
## Swarm Execution Rules
|
||||
|
||||
- ALWAYS use `run_in_background: true` for all Agent tool calls
|
||||
- ALWAYS put ALL Agent calls in ONE message for parallel execution
|
||||
- After spawning, STOP — do NOT add more tool calls or check status
|
||||
- Never poll agent status repeatedly — trust agents to return
|
||||
- Review ALL results before proceeding
|
||||
|
||||
## 3-Tier Model Routing (ADR-026)
|
||||
|
||||
| Tier | Handler | Cost | Use Cases |
|
||||
|------|---------|------|-----------|
|
||||
| **1** | Edit tool directly | $0 | Single-line transforms — skip LLM |
|
||||
| **2** | Haiku | $0.0002 | Simple tasks (<30% complexity) |
|
||||
| **3** | Sonnet/Opus | $0.003–0.015 | Complex reasoning, architecture, security |
|
||||
|
||||
## Ruflo Memory & Shared Context
|
||||
|
||||
Keep shared context between Claude and Codex via Ruflo memory so Codex always has full task specs:
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `memory_store` | Store design decisions and task specs |
|
||||
| `memory_search` | Semantic search before starting work |
|
||||
| `memory_search_unified` | Search across Claude + AgentDB + patterns |
|
||||
| `memory_retrieve` | Retrieve a stored spec by key |
|
||||
|
||||
```bash
|
||||
# Store a design decision before delegating
|
||||
npx @claude-flow/cli@latest memory store --key "task-<name>" --value "<full spec>" --namespace tasks
|
||||
|
||||
# Search for prior patterns
|
||||
npx @claude-flow/cli@latest memory search --query "<feature keyword>"
|
||||
```
|
||||
|
||||
## Key MCP Tools (discover via ToolSearch)
|
||||
|
||||
| Category | Tools |
|
||||
|----------|-------|
|
||||
| Memory | `memory_store`, `memory_search`, `memory_search_unified` |
|
||||
| Swarm | `swarm_init`, `swarm_status`, `swarm_health` |
|
||||
| Agents | `agent_spawn`, `agent_list`, `agent_status` |
|
||||
| Hive-Mind | `hive-mind_init`, `hive-mind_spawn`, `hive-mind_consensus` |
|
||||
| Hooks | `hooks_route`, `hooks_session-start`, `hooks_post-task` |
|
||||
| Security | `aidefence_scan`, `aidefence_is_safe` |
|
||||
|
||||
```
|
||||
ToolSearch("memory search") → memory_store, memory_search, memory_search_unified
|
||||
ToolSearch("swarm") → swarm_init, swarm_status, swarm_health
|
||||
ToolSearch("+aidefence") → aidefence_scan, aidefence_is_safe, aidefence_has_pii
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
- Documentation: https://github.com/ruvnet/ruflo
|
||||
- Issues: https://github.com/ruvnet/ruflo/issues
|
||||
@@ -0,0 +1,199 @@
|
||||
# SYSADMIN CHRONICLES — OPEN ISSUES
|
||||
> Version 2.0 | Last updated: Phase 1 skeleton build
|
||||
>
|
||||
> All known design gaps, content bugs, and deferred decisions.
|
||||
> Items here must NOT be implemented with a best-guess — wait for a resolution.
|
||||
> Mark items RESOLVED with the fix details when closed.
|
||||
|
||||
---
|
||||
|
||||
## AGENT INSTRUCTIONS (READ FIRST)
|
||||
> Added during Phase 1 skeleton build. Document any decisions you make here.
|
||||
>
|
||||
> When you resolve an open issue:
|
||||
> 1. Move it to the RESOLVED ISSUES section at the bottom.
|
||||
> 2. Add resolution details and the file(s) changed.
|
||||
> 3. Update ROADMAP.md to mark relevant tasks complete.
|
||||
>
|
||||
> When you make a minor direction change (non-game-changing):
|
||||
> 1. Note it here under NEW DECISIONS.
|
||||
> 2. Update the relevant doc (ARCHITECTURE.md, SAVE_SYSTEM.md, etc.)
|
||||
> 3. Do NOT silently patch content files — note it here first.
|
||||
>
|
||||
> **CODE QUALITY AUDIT — COMPLETE**
|
||||
> All P1, P2, and P3 items from docs/CODEX_AUDIT_FIXES.md have been resolved.
|
||||
> docs/CODEX_AUDIT_FIXES.md has been deleted per its own WHEN DONE instruction.
|
||||
|
||||
---
|
||||
|
||||
## NEW DECISIONS (Made During Phase 1 Build)
|
||||
|
||||
### ND-001 — T005-T008 bundled file needs split (same as OI-008 pattern)
|
||||
The file T005-T008.json is a bundled array. Content loader expects one file per
|
||||
ticket with an "id" field. It has been kept as-is (content loader skips arrays).
|
||||
Split into T005.json–T008.json before Phase 5 ticket loading is implemented.
|
||||
|
||||
### ND-002 — OI-006 (persists: false) already resolved in SAVE_SYSTEM.md v1.3
|
||||
SAVE_SYSTEM.md v1.3 defines shift-boundary reset. SaveState.js implements the
|
||||
reset_shift_flags() equivalent at shift start. Closed.
|
||||
|
||||
### ND-003 — OI-007 (Q002 blast_radius) safe to fix mechanically
|
||||
Q002-syntax-error.json blast_radius: ["I001"] should be []. Mechanical fix —
|
||||
the next agent can apply it directly without asking. See OI-007.
|
||||
|
||||
### ND-004 — Stack is Node.js + Svelte
|
||||
The game runs as a Node.js/Express server with a Svelte web HUD. The workstation
|
||||
is a real XFCE VM (sc-workstation). All game logic lives in `server/src/`.
|
||||
|
||||
### ND-005 — opsbridge/sudo SSH path for host→workstation validation
|
||||
The validation path from the host uses the `opsbridge` management user then
|
||||
`sudo -H -i -u player` inside the guest, because Q001 intentionally removes
|
||||
`/home/player/.ssh/authorized_keys`. The correct form is:
|
||||
|
||||
```
|
||||
ssh opsbridge@<guest> sudo -H -i -u player -- sh -c '<command>'
|
||||
```
|
||||
|
||||
Separated sudo flags (`-H -i -u`) required — combined `-Hiu` misparses on some builds.
|
||||
|
||||
**Status**: RESOLVED — confirmed working in ValidationEngine.js SSH path.
|
||||
|
||||
### ND-006 — build_machine snapshot chain now materializes baseline.post-q006 from Q006 clean state
|
||||
`tools/setup/seed-vms.sh` now builds `sc-build-machine` in two authored stages:
|
||||
`Q006-prep.sh` creates the broken `baseline.clean` state for "Time Is A Flat
|
||||
Circle", and `Q006-post-clean.sh` applies the clean branch outcome before taking
|
||||
`baseline.post-q006`.
|
||||
|
||||
`Q008` is still a separate multi-VM provisioning gap. Its authored starting
|
||||
state touches both vulcan and hermes, so it should not be guessed into the
|
||||
single-domain snapshot chain until that flow is designed explicitly.
|
||||
|
||||
### ND-007 — terminal UX: Tilix is the player's terminal (no in-game simulation)
|
||||
**Status**: RESOLVED. The player uses a real Tilix terminal inside the workstation
|
||||
XFCE VM. All terminal UX (history, scrollback, copy/paste) is handled by Tilix.
|
||||
No terminal simulation needed. See `docs/WORKSTATION_POLISH_BACKLOG.md` for
|
||||
outstanding workstation desktop polish items.
|
||||
|
||||
### ND-008 — vulcan player shell/PATH is still misprovisioned
|
||||
**Status**: RESOLVED 2026-04-24.
|
||||
Root cause: `inetutils` (provides `/usr/bin/hostname` on Arch) was not in the
|
||||
`build-build-machine.sh` pacman install. Hermes (Debian) has hostname pre-installed.
|
||||
Fix applied in `tools/vm/build-build-machine.sh`:
|
||||
- Added `inetutils` to the runcmd pacman install line.
|
||||
- Added runcmd entries to write `/home/player/.bashrc` (explicit PATH) and
|
||||
`.bash_profile` (sources .bashrc), then chown to player.
|
||||
Regression gate added to `tools/setup/seed-vms.sh` (STEP 1b): after builds,
|
||||
SSH-tests `hostname` on sc-web-server and sc-build-machine; fails fast if missing.
|
||||
|
||||
---
|
||||
|
||||
## MUST RESOLVE BEFORE PHASE 3
|
||||
|
||||
### OI-001 — Q001 permissive-setup branch contradictory logic
|
||||
**File**: content/quests/Q001-welcome-aboard.json
|
||||
Option A: bad-but-not-fatal permissions (755 dir), quest completes with warning.
|
||||
Option B: fatally wrong permissions (777), quest does NOT complete via this branch.
|
||||
**Decision needed**: Which option?
|
||||
**Status**: RESOLVED — permissive-setup branch (Option A/lenient) was already correctly implemented. Q001 branch validates file_exists + file_owner without checking mode, so 755 directory case completes the quest with trust_delta 0. marcus-Q001.json already has complete-permissive stage.
|
||||
|
||||
### OI-002 — Q008 rollback-only vs rollback-and-pin have identical validation
|
||||
**File**: content/quests/Q008-bad-upstream.json
|
||||
Need a distinguishing rule for pinned vs unpinned. Likely an IgnorePkg entry
|
||||
in /etc/pacman.conf (detectable via file_contains).
|
||||
**Status**: RESOLVED — Q008 already has file_contains check for IgnorePkg in /etc/pacman.conf on rollback-and-pin branch, and a not-rule on the rollback-only branch to ensure mutual exclusion. Confirmed in Q008 internal_notes.
|
||||
|
||||
---
|
||||
|
||||
## MUST RESOLVE BEFORE PHASE 5
|
||||
|
||||
### OI-003 — Incident files I002 and I003 are missing
|
||||
Author I002-backup-pressure-recurrence.json and I003-app-update-recurrence.json
|
||||
following the I001 pattern before Phase 6.
|
||||
**Status**: RESOLVED — both files authored (content/incidents/I002-backup-pressure-recurrence.json, I003-app-update-recurrence.json). Content validator passes zero errors.
|
||||
|
||||
### OI-004 — pressure_profile field is referenced but never defined
|
||||
Recommend: separate files in content/pressure_profiles/ with a defined schema.
|
||||
**Status**: RESOLVED — created content/pressure_profiles/ with web_outage_escalation.json and app_outage_escalation.json. Schema uses trigger_after_seconds steps with notification, notification_severity, and escalate_linked_ticket fields. escalate_linked_ticket resolves to the quest's own ticket_id at runtime.
|
||||
|
||||
### OI-005 — check_mode: explicit trigger mechanism undefined
|
||||
A "Verify Fix" button in the ticket panel UI, shown per-objective when check_mode == explicit.
|
||||
**Status**: RESOLVED — Verify Fix button implemented in TicketsPanel.svelte. Button appears
|
||||
per-objective when check_mode == explicit, disables during check, re-enables with 2s delay on failure.
|
||||
|
||||
---
|
||||
|
||||
## LOW PRIORITY / ANYTIME
|
||||
|
||||
### OI-007 — Q002 blast_radius incorrectly references I001
|
||||
Fix: change blast_radius: ["I001"] to blast_radius: [] in Q002.
|
||||
**Status**: RESOLVED — blast_radius set to [] in Q002-syntax-error.json; _blast_radius_note added explaining I001 triggers only from Q003 quick-fix branch.
|
||||
|
||||
### OI-008 — tier2-dialogue.json naming convention
|
||||
Individual files exist: marcus-Q005.json, marcus-Q006.json, marcus-Q008.json,
|
||||
priya-Q007.json. The bundled file is kept as tier2-dialogue.SPLIT_PENDING.json.
|
||||
Verify individual files are complete then delete the SPLIT_PENDING file.
|
||||
**Status**: RESOLVED — individual files confirmed present. Bundled file removed.
|
||||
|
||||
### OI-009 — sarah-web series has only one member
|
||||
sarah-Q003-angry.json declares series_id: "sarah-web" but no second member exists.
|
||||
Either add sarah-Q004+ or remove series_id until a second file is authored.
|
||||
**Status**: RESOLVED — series_id and series_position removed from sarah-Q003-angry.json. Series grouping deferred until a second sarah-web member is authored.
|
||||
|
||||
### OI-011 — Snapshot baseline chain
|
||||
seed-vms.sh implements the chain. Need formal policy in QUEST_AUTHORING.md.
|
||||
Chain: workstation: baseline.day-one; web_server: clean→post-q002→post-q003→post-q004;
|
||||
build_machine: clean→post-q006. Each post-qXXX baseline from CLEAN branch resolution.
|
||||
**Status**: RESOLVED — Baseline Snapshot Chain subsection added to docs/QUEST_AUTHORING.md in VM PROVISIONING HOOKS section. Documents chain per VM, the clean-branch-only rule, and naming convention.
|
||||
|
||||
---
|
||||
|
||||
## RESOLVED ISSUES
|
||||
|
||||
### OI-006 — persists: false flag semantics
|
||||
Resolution: Shift-boundary reset handled in SaveState.js at shift start.
|
||||
|
||||
### OI-010 — file_absent and file_owner_is_not undocumented rule types
|
||||
Resolution: Added to ValidationEngine.js as full rule types.
|
||||
Still needs: update QUEST_AUTHORING.md rule reference table.
|
||||
|
||||
### OI-012 — SSH execution contract
|
||||
Resolution: server/src/lib/ssh.js — Promise-based, structured result (stdout/stderr/exitCode),
|
||||
BatchMode key-based auth, 30s default timeout.
|
||||
|
||||
### OI-013 — Language choice
|
||||
Resolution: Node.js + Svelte. See ND-004.
|
||||
|
||||
---
|
||||
|
||||
## ADDITIONAL RESOLUTIONS (Phase 1 continued)
|
||||
|
||||
### OI-003 — I002 and I003 incident files authored
|
||||
**Resolution**:
|
||||
- I002-backup-pressure-recurrence.json authored — triggers on hermes_backup_partial flag,
|
||||
3-step escalation, resolves when cron+ownership+disk all correct.
|
||||
- I003-app-update-recurrence.json authored — triggers when rollback-only branch taken on Q008,
|
||||
re-installs broken version unless pinned. Resolves when IgnorePkg + correct version confirmed.
|
||||
- Content validator now passes zero errors.
|
||||
|
||||
### ND-001 — T005-T008 split complete
|
||||
T005.json, T006.json, T007.json, T008.json created from the bundled file.
|
||||
Content validator now loads all 8 tickets correctly.
|
||||
|
||||
### OI-007 — Q002 blast_radius fix
|
||||
**Resolution**: The validator was fixed to normalize world_flags.json array format.
|
||||
The Q002 blast_radius: ["I001"] issue is documented — apply this one-line fix
|
||||
directly: change blast_radius in Q002-syntax-error.json from ["I001"] to [].
|
||||
|
||||
### VALIDATOR FIXES applied this session:
|
||||
- validate-content.js now normalizes world_flags.json array format correctly
|
||||
- Advisory clue_fingerprint rule types (service_state_is, file_size_above, etc.)
|
||||
are now accepted — they describe evidence, not runtime-evaluated rules
|
||||
- T005-T008 bundled file is now skipped correctly (SPLIT_DONE suffix)
|
||||
- WorldFlags handling now normalizes both Array and Dict flag formats
|
||||
|
||||
### CONTENT STATUS: validate-content.js exits 0 (zero errors, 2 warnings)
|
||||
Warnings are all expected and documented:
|
||||
- priya-ops series: 1 member (needs future dialogue)
|
||||
- T005-T008.SPLIT_DONE.json: skipped (bundled file, split done)
|
||||
(sarah-web series warning removed — series_id stripped from sarah-Q003-angry.json per OI-009)
|
||||
(tier2-dialogue.SPLIT_PENDING warning removed — renamed to .SPLIT_DONE.bak per OI-008)
|
||||
@@ -0,0 +1,255 @@
|
||||
# PROJECT_MAP.md
|
||||
|
||||
# Auto-generated / agent-maintained project index.
|
||||
# Purpose: help future agents quickly select the right context and files.
|
||||
# Last updated: 2026-04-30 - Initial root map created from current repo and docs.
|
||||
|
||||
---
|
||||
|
||||
## 1. Project Snapshot
|
||||
|
||||
Sysadmin Chronicles is a Linux sysadmin game where players resolve tickets inside real libvirt/QEMU VMs. The host Node.js/Express server owns state, validation, VM control, APIs, and static sites; the Svelte/Vite HUD runs in Chromium inside the workstation VM.
|
||||
|
||||
---
|
||||
|
||||
## 2. Context Budget Rules
|
||||
|
||||
1. Read this `PROJECT_MAP.md` first.
|
||||
2. Identify the relevant hot path or feature area.
|
||||
3. Load only directly relevant files.
|
||||
4. Load one dependency layer outward only if needed.
|
||||
5. Avoid loading whole directories unless the map says the area is tightly coupled.
|
||||
6. Prefer tests and public interfaces over implementation details when scoping behavior.
|
||||
7. Update this map after meaningful structural or user-facing changes.
|
||||
|
||||
Do not paste large files into context when a targeted excerpt, `rg`, `rtk`, or symbol search is enough.
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture Summary
|
||||
|
||||
The game is host-authoritative: frontend actions call server APIs, services update save/game state, and validation checks real VM state over SSH/libvirt instead of trusting typed commands.
|
||||
|
||||
Runtime flow:
|
||||
|
||||
`scripts/start-game.sh -> server/src/index.js -> ContentLoader/SaveState/services -> VMManager.ensureWorkstationLive() -> Express/WebSocket on port 3000 -> remote-viewer opens sc-workstation -> Chromium HUD`
|
||||
|
||||
VMs:
|
||||
|
||||
- `sc-workstation` / `ares`: player workstation, XFCE, Chromium HUD, Tilix.
|
||||
- `sc-web-server` / `hermes`: web/server target.
|
||||
- `sc-build-machine` / `vulcan`: build/package target.
|
||||
|
||||
---
|
||||
|
||||
## 4. File Priority Map
|
||||
|
||||
### Tier 1 - Critical / frequently needed
|
||||
|
||||
| Path | Role | When to load |
|
||||
|------|------|--------------|
|
||||
| `server/src/index.js` | Server bootstrap, routes, static serving, WebSocket events | Runtime/API/static route changes |
|
||||
| `server/src/services/ContentLoader.js` | Loads authored content collections | Content schema/loading changes |
|
||||
| `server/src/services/SaveState.js` | Save persistence and state shape | Save/progression compatibility changes |
|
||||
| `server/src/services/QuestEngine.js` | Quest lifecycle and completion | Quest activation/completion changes |
|
||||
| `server/src/services/TicketService.js` | Ticket state and resolution | Ticket workflow changes |
|
||||
| `server/src/services/ValidationEngine.js` | Real VM rule evaluation | Objective/validation rule changes |
|
||||
| `server/src/services/VMManager.js` | libvirt state, startup, IP discovery | VM runtime/control changes |
|
||||
| `frontend/src/App.svelte` | Main HUD orchestration | Tab/workflow/UI state changes |
|
||||
| `frontend/src/lib/api.js` | Browser API client/session handling | API contract changes |
|
||||
| `tools/vm/build-vm.sh` | Common VM build driver | VM build behavior changes |
|
||||
| `tools/vm/profiles/workstation.sh` | Ares workstation image profile | Desktop/provisioning changes |
|
||||
| `scripts/start-game.sh`, `start-game.sh` | End-to-end launchers | Startup/viewer/server launch changes |
|
||||
| `tools/lib/internal-https.sh` | Shared internal HTTPS cert/env/URL helpers | Portal/Sage/company URL or TLS startup changes |
|
||||
| `content/quests/`, `content/tickets/`, `content/vm_profiles/` | Authored gameplay data | Quest, ticket, or VM identity changes |
|
||||
|
||||
### Tier 2 - Supporting / sometimes needed
|
||||
|
||||
| Path | Role | When to load |
|
||||
|------|------|--------------|
|
||||
| `server/src/routes/` | API route modules | Endpoint behavior changes |
|
||||
| `server/src/services/TrustSystem.js` | Trust score and unlocks | Access/progression tuning |
|
||||
| `server/src/services/ProgressionSystem.js` | Unlock/progression rules | Unlock state changes |
|
||||
| `server/src/services/EmailService.js` | Mail/read/reply state | Mail workflow changes |
|
||||
| `server/src/services/SageService.js` | Sage API behavior | Knowledge-base changes |
|
||||
| `server/src/services/IncidentScheduler.js` | Timed incident pressure | Incident/shift pacing |
|
||||
| `server/src/services/ShiftReviewService.js` | End-shift review data | Profile/review changes |
|
||||
| `frontend/src/components/` | HUD panels/components | Focused UI changes |
|
||||
| `tools/content/validate-content.js` | Content validator | Any content schema/rule change |
|
||||
| `tools/setup/*.sh` | Host setup/seed/uninstall | Installer/setup flow changes |
|
||||
| `tools/vm/profiles/*.sh` | Target VM profiles | Hermes/vulcan/workstation provisioning changes |
|
||||
| `tools/vm/repair-workstation-launchers.sh` | Live workstation desktop launcher trust repair | Existing Ares desktop launcher prompt fixes |
|
||||
| `tools/vm/quest-prep/` | Baseline quest state authorship | VM baseline quest setup |
|
||||
| `sage/`, `company-website/` | Static web surfaces | Sage/company site changes |
|
||||
| `docs/ARCHITECTURE.md`, `docs/SAVE_SYSTEM.md`, `docs/VM_BUILD_SYSTEM.md` | Deep design docs | Architecture/save/build questions |
|
||||
|
||||
### Tier 3 - Peripheral / rarely needed
|
||||
|
||||
| Path | Role | When to load |
|
||||
|------|------|--------------|
|
||||
| `frontend/dist/` | Generated frontend build | Only to verify served assets |
|
||||
| `node_modules/` | Installed dependencies | Avoid; use manifests instead |
|
||||
| `ruvector.db` | Local RTK/vector data | Do not inspect for normal work |
|
||||
| `vm/images/`, `vm/snapshots/` | Large/live VM data | Only for explicit VM storage tasks |
|
||||
| Static assets | Images/icons/fonts | Only for visual asset changes |
|
||||
|
||||
---
|
||||
|
||||
## 5. Hot Paths
|
||||
|
||||
- Change server API route:
|
||||
- Load: `server/src/index.js`, relevant `server/src/routes/*`, relevant service, `frontend/src/lib/api.js`
|
||||
- Usually update: server tests and frontend caller
|
||||
- Watch out for: session middleware and WebSocket refresh expectations
|
||||
|
||||
- Change quest/ticket behavior:
|
||||
- Load: `content/quests/`, `content/tickets/`, `ContentLoader.js`, `QuestEngine.js`, `TicketService.js`, `ValidationEngine.js`
|
||||
- Usually update: `tools/content/validate-content.js`, docs if schema changes
|
||||
- Watch out for: world flag and dialogue ID references
|
||||
|
||||
- Change validation rule:
|
||||
- Load: `ValidationEngine.js`, tests, representative quest JSON
|
||||
- Usually update: `docs/QUEST_AUTHORING.md`
|
||||
- Watch out for: validation must observe real VM state, not commands typed
|
||||
|
||||
- Change VM build/provisioning:
|
||||
- Load: `docs/VM_BUILD_SYSTEM.md`, `tools/vm/build-vm.sh`, relevant `tools/vm/profiles/*.sh`, `tools/setup/seed-vms.sh`
|
||||
- Usually update: setup docs and dependency/version tracking
|
||||
- Watch out for: destructive `--force`, cloud-init quoting, readiness gates
|
||||
|
||||
- Change workstation desktop UX:
|
||||
- Load: `tools/vm/profiles/workstation.sh`, `scripts/start-game.sh`, `runtime/viewer/*`
|
||||
- Usually update: rebuild/patch instructions
|
||||
- Watch out for: RAM pressure, browser launch, desktop icon permissions
|
||||
|
||||
- Change frontend HUD workflow:
|
||||
- Load: `frontend/src/App.svelte`, relevant component, `frontend/src/lib/api.js`
|
||||
- Usually update: `cd frontend && npm run build`
|
||||
- Watch out for: generated `frontend/dist` is what the server serves
|
||||
|
||||
- Change save/progression/trust:
|
||||
- Load: `SaveState.js`, `TrustSystem.js`, `ProgressionSystem.js`, `QuestEngine.js`, content progression JSON
|
||||
- Usually update: migration/default handling and tests
|
||||
- Watch out for: no-auto-restore and backward compatibility
|
||||
|
||||
- Change Sage/company web surface:
|
||||
- Load: `server/src/index.js`, `sage/` or `company-website/`, workstation profile bookmarks/proxy config
|
||||
- Usually update: browser smoke test inside ares
|
||||
- Watch out for: `/sage/` route and guest bookmark expectations
|
||||
|
||||
---
|
||||
|
||||
## 6. Change Impact Map
|
||||
|
||||
| Change type | Also check/update |
|
||||
|-------------|-------------------|
|
||||
| API contract | route, service, `frontend/src/lib/api.js`, HUD state handling, tests |
|
||||
| Content schema or IDs | validator, ContentLoader, quests/tickets/dialogue/world flags, docs |
|
||||
| Validation rule | `ValidationEngine.js`, representative content, tests, authoring docs |
|
||||
| Persistence format | `SaveState.js`, migrations/defaults, load/save compatibility tests |
|
||||
| VM profile/domain | setup scripts, start script, content VM profiles, docs, live libvirt state |
|
||||
| Workstation desktop | profile packages, cloud-init user-data, browser/shortcut config, RAM/performance |
|
||||
| Frontend UI workflow | `App.svelte`, component state, API client, WebSocket refresh behavior |
|
||||
| Build/dev tooling | README/dev docs, `AGENTS.md`, scripts, dependency/version manifest |
|
||||
|
||||
---
|
||||
|
||||
## 7. Key Concepts & Domain Terms
|
||||
|
||||
- **Ares**: player workstation VM; libvirt domain `sc-workstation`.
|
||||
- **Hermes**: web server target VM; libvirt domain `sc-web-server`.
|
||||
- **Vulcan**: build machine target VM; libvirt domain `sc-build-machine`.
|
||||
- **opsbridge**: management user for host-driven SSH validation/control.
|
||||
- **player**: in-world workstation user.
|
||||
- **world_flags**: durable flags for quest/narrative branching.
|
||||
- **trust/unlocks**: score and capability gate system.
|
||||
- **solution_branches**: authored ticket outcomes.
|
||||
- **pressure_profiles**: incident/timer pressure configuration.
|
||||
- **baseline/recovery/checkpoint/live**: VM state tiers described in save/snapshot docs.
|
||||
- **Sage**: knowledge-base/help system.
|
||||
|
||||
---
|
||||
|
||||
## 8. User-Facing Surface
|
||||
|
||||
- Launch: `bash scripts/start-game.sh`.
|
||||
- HUD tabs: Tickets, Mail, Docs, Sage, VMs, Profile.
|
||||
- Server APIs: `/api/session`, `/api/state`, `/api/tickets`, `/api/mail`, `/api/docs`, `/api/vms`, `/api/sage`, `/api/profile`.
|
||||
- Static sites: `/sage`, `/company`, `/public`; intended in-VM browser URLs use HTTPS (`portal.axiomworks.internal:3000`, `sage.axiomworks.internal:3000/sage/`, `www.axiomworks.corp/`).
|
||||
- Workstation desktop: Chromium HUD, terminal, desktop shortcuts, remote-viewer session.
|
||||
- Save path: `~/.local/share/sysadmin-chronicles/save.json`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Persistence / Data Contracts
|
||||
|
||||
| Contract | Defined in | Compatibility notes |
|
||||
|----------|------------|---------------------|
|
||||
| Save file | `SaveState.js`, docs `SAVE_SYSTEM.md` | Preserve load defaults and migration behavior |
|
||||
| Authored content JSON | `content/`, `ContentLoader.js`, validator | Treat as read-only runtime input; keep IDs stable |
|
||||
| VM profiles | `content/vm_profiles/`, `tools/vm/profiles/*.sh` | Domain/hostname/user changes affect scripts and docs |
|
||||
| Frontend session token | `frontend/src/lib/api.js` | Stored in browser local storage; API retries on invalid session |
|
||||
| VM disks/snapshots | libvirt/qcow2, docs | Save/load must handle missing domains/snapshots gracefully |
|
||||
| Static route prefixes | `server/src/index.js` | Guest bookmarks/proxies may depend on `/sage` and `/company` |
|
||||
|
||||
---
|
||||
|
||||
## 10. Test & Validation Map
|
||||
|
||||
| Change area | Validation |
|
||||
|-------------|------------|
|
||||
| Content changes | `node tools/content/validate-content.js --verbose` |
|
||||
| Server services/routes | `cd server && npm test` |
|
||||
| Frontend HUD | `cd frontend && npm run build` |
|
||||
| Host setup | `bash tools/setup/check-host.sh` |
|
||||
| VM profile/build logic | `bash tools/vm/build-workstation.sh --dry-run` when available; otherwise inspect generated command/output |
|
||||
| Live VM runtime | `virsh --connect qemu:///system list --all`; targeted SSH/HTTP checks |
|
||||
| Full smoke test | `bash scripts/start-game.sh`, then use HUD in ares workstation |
|
||||
|
||||
---
|
||||
|
||||
## 11. Known Risk Areas / Tech Debt
|
||||
|
||||
- **VM rebuilds**: build scripts can destroy/recreate `sc-` domains; confirm intent before force paths.
|
||||
- **Cloud-init profiles**: YAML and shell quoting are fragile in `tools/vm/profiles/*.sh`.
|
||||
- **Readiness checks**: networking, cloud-init, LightDM, and SSH can hang builds if guest state drifts.
|
||||
- **Save/snapshot drift**: save refs can point at missing domains or snapshots; recovery handling matters.
|
||||
- **Real VM validation**: many behaviors need live libvirt smoke tests beyond unit tests.
|
||||
- **Content cross-references**: IDs, world flags, dialogue, branches, and tickets can silently desync.
|
||||
- **Generated frontend**: server serves `frontend/dist` when present, so rebuild after UI changes.
|
||||
- **Workstation performance**: Chromium/XFCE can cause RAM pressure and perceived hangs.
|
||||
- **Internal HTTPS**: `tools/lib/internal-https.sh` is the shared source for launcher TLS env and in-VM Portal/Sage/company URLs; avoid reintroducing per-script HTTP fallbacks.
|
||||
- **Desktop launcher trust**: Trust all `/home/player/Desktop/*.desktop` files through the real player DBus session via `/usr/local/bin/trust-desktop-launchers`; use `tools/vm/repair-workstation-launchers.sh` for live VMs.
|
||||
- **Docs drift**: some older roadmap/status docs may lag active implementation.
|
||||
|
||||
---
|
||||
|
||||
## 12. Anti-Patterns
|
||||
|
||||
- Do not fake SSH, terminals, or validation results for core gameplay.
|
||||
- Do not validate quests by matching commands the player typed.
|
||||
- Do not operate on non-`sc-` libvirt domains from game scripts.
|
||||
- Do not mutate `content/` as runtime save state.
|
||||
- Do not run quest-prep scripts against live player VMs unless explicitly intended.
|
||||
- Do not write save JSON ad hoc; go through `SaveState.js`.
|
||||
- Do not rely only on QEMU guest agent IP discovery.
|
||||
- Do not place scratch files in the repo root.
|
||||
- Do not turn this map into a changelog or README replacement.
|
||||
|
||||
---
|
||||
|
||||
## 13. Agent Workflow Notes
|
||||
|
||||
1. Start with this map.
|
||||
2. Use hot paths to choose files.
|
||||
3. Prefer symbol/search-based context over broad file loading.
|
||||
4. Keep edits narrow.
|
||||
5. Update this map only when structure, ownership, contracts, workflows, or known risk areas change.
|
||||
6. Append a one-line changelog entry for meaningful updates.
|
||||
|
||||
---
|
||||
|
||||
## 14. Change Log
|
||||
|
||||
- 2026-04-30 Initial root map created; updated VM tooling notes for seed ISO detach and installer/rebuild image path normalization.
|
||||
- 2026-05-02 Centralized internal HTTPS launch URLs/TLS env and workstation desktop launcher trust repair paths.
|
||||
@@ -0,0 +1,155 @@
|
||||
# Sysadmin Chronicles
|
||||
|
||||
A native Linux game where you work as a junior sysadmin at Axiom Works, handling
|
||||
real tickets inside real Linux virtual machines managed by QEMU/KVM.
|
||||
|
||||
**Status**: Node.js server + Svelte HUD implemented. Server, frontend, and all
|
||||
services are built. Pending: Phase 7 workstation VM verification + Phase 10 full playtest.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Summary
|
||||
|
||||
The game runs as a Node.js server on the host, serving a Svelte web HUD into the
|
||||
workstation VM's browser. The player works inside a real XFCE desktop.
|
||||
|
||||
```
|
||||
Host machine
|
||||
├── Node.js game server (port 3000) — quest logic, validation, VM control
|
||||
└── Svelte HUD — tickets, mail, Sage, docs (served by game server)
|
||||
|
||||
Workstation VM (sc-workstation / ares) — Debian 12 XFCE desktop
|
||||
├── Chromium → http://192.168.100.1:3000 (HUD, auto-opens on login)
|
||||
└── Tilix → SSH to hermes/vulcan (real terminal, real SSH)
|
||||
|
||||
Target VMs (headless)
|
||||
├── sc-web-server (hermes) — Q002–Q005, Q007
|
||||
└── sc-build-machine (vulcan) — Q006, Q008
|
||||
```
|
||||
|
||||
Quest completion is validated by the server SSHing into the target VM and
|
||||
evaluating real system state — not by tracking commands typed.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start (Development)
|
||||
|
||||
### Prerequisites
|
||||
|
||||
```bash
|
||||
# Install host dependencies
|
||||
sudo apt install qemu-system-x86_64 libvirt-daemon-system virsh qemu-img \
|
||||
nodejs npm virt-viewer
|
||||
|
||||
# Add yourself to the libvirt group
|
||||
sudo usermod -aG libvirt $USER && newgrp libvirt
|
||||
```
|
||||
|
||||
### First-Time Setup
|
||||
|
||||
```bash
|
||||
# Check host capabilities
|
||||
bash tools/setup/check-host.sh
|
||||
|
||||
# Create libvirt networks, storage pool, and SSH keys
|
||||
bash tools/setup/first-run-setup.sh
|
||||
|
||||
# Build VM images and provision quest baselines
|
||||
bash tools/setup/seed-vms.sh
|
||||
```
|
||||
|
||||
### Build the Frontend
|
||||
|
||||
```bash
|
||||
cd frontend && npm install && npm run build && cd ..
|
||||
```
|
||||
|
||||
### Run the Game
|
||||
|
||||
```bash
|
||||
# Start game server + open workstation VM via SPICE
|
||||
bash scripts/start-game.sh
|
||||
|
||||
# Or run server only (for development/testing)
|
||||
cd server && npm install && node src/index.js
|
||||
```
|
||||
|
||||
### Validate Content
|
||||
|
||||
```bash
|
||||
node tools/content/validate-content.js --verbose
|
||||
```
|
||||
|
||||
### Run Server Tests
|
||||
|
||||
```bash
|
||||
cd server && npm test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
sysadmin-chronicles/
|
||||
│
|
||||
├── server/ Node.js game server
|
||||
│ └── src/
|
||||
│ ├── index.js Entry point — Express + WebSocket
|
||||
│ ├── routes/ REST API routes
|
||||
│ └── services/ ContentLoader, QuestEngine, ValidationEngine, etc.
|
||||
│
|
||||
├── frontend/ Svelte web HUD
|
||||
│ ├── src/ Components, api.js
|
||||
│ └── dist/ Built output (served by game server)
|
||||
│
|
||||
├── scripts/
|
||||
│ └── start-game.sh Start server + open SPICE viewer
|
||||
│
|
||||
├── content/ All game content (JSON — unchanged)
|
||||
│ ├── quests/ Q001–Q008
|
||||
│ ├── tickets/ T001–T008
|
||||
│ ├── incidents/ I001–I003
|
||||
│ ├── dialogue/ All NPC dialogue files
|
||||
│ ├── vm_profiles/ workstation, web_server, build_machine
|
||||
│ └── progression/ trust_unlocks.json
|
||||
│
|
||||
├── tools/
|
||||
│ ├── setup/ check-host.sh, first-run-setup.sh, seed-vms.sh
|
||||
│ ├── vm/ build scripts, quest-prep/, suppress-maintenance-noise.sh
|
||||
│ └── content/ validate-content.js, verify-clue-fingerprints.js
|
||||
│
|
||||
├── docs/
|
||||
│ ├── ARCHITECTURE.md System design
|
||||
│ ├── ROADMAP.md Phase tracking
|
||||
│ └── QUEST_AUTHORING.md Content authoring guide
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Design Rules
|
||||
|
||||
- Game server is the single source of truth — frontend only displays results
|
||||
- Validation is server-side only — SSH into VMs, evaluate real system state
|
||||
- Quest completion is state-based only — never command-sequence tracking
|
||||
- Only operate on `sc-` prefixed libvirt domains
|
||||
- Content JSON is read-only at runtime — ContentLoader reads once at startup
|
||||
- Save file is at `~/.local/share/sysadmin-chronicles/save.json`
|
||||
|
||||
---
|
||||
|
||||
## Current Build State
|
||||
|
||||
### Done
|
||||
- Node.js game server with all services (ContentLoader, QuestEngine, TicketService,
|
||||
ValidationEngine, VMManager, TrustSystem, ProgressionSystem, EmailService,
|
||||
SageService, ShiftTimer, IncidentScheduler, ShiftReviewService, CertificationService)
|
||||
- All REST routes (tickets, mail, docs, sage, state, vms, session)
|
||||
- Svelte frontend with all panels (Tickets, Mail, Docs, Sage, VMs, Header)
|
||||
- Built frontend (`frontend/dist/`) served by game server
|
||||
- Content: Q001–Q008, T001–T008, I001–I003, all dialogue, world_flags, trust_unlocks
|
||||
- Content validator: `validate-content.js` exits zero
|
||||
|
||||
### Pending
|
||||
- Phase 7: verify XFCE workstation VM (SPICE display, Chromium autostart, Tilix default)
|
||||
- Phase 10: full end-to-end playtest (Q001→Q002 with real VMs)
|
||||
@@ -0,0 +1,31 @@
|
||||
# RTK Usage
|
||||
|
||||
`rtk` is installed at `/home/aaron/.cargo/bin/rtk`. Use it by default for noisy shell commands so agent sessions spend fewer tokens on low-value output.
|
||||
|
||||
Prefer `rtk` for:
|
||||
|
||||
- directory and file discovery: `rtk ls`, `rtk tree`, `rtk find`
|
||||
- search output: `rtk grep`
|
||||
- tests and builds with noisy output: `rtk test`, `rtk npm test`, `rtk tsc`, `rtk lint`
|
||||
- dependency and environment summaries: `rtk deps`, `rtk env`
|
||||
- logs, JSON, diffs, and command summaries: `rtk log`, `rtk json`, `rtk diff`, `rtk summary`
|
||||
|
||||
Use raw shell commands instead when exact, unfiltered, streaming, or interactive output matters. Examples: `sed -n` for precise line ranges, `tail -f` for live logs, `virsh console`, prompts, password entry, TTY tools, and commands whose full output is the thing being inspected.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
rtk ls -la
|
||||
rtk tree -L 2
|
||||
rtk grep "READY_COMMAND" tools/vm
|
||||
rtk npm test
|
||||
rtk deps
|
||||
```
|
||||
|
||||
Useful checks:
|
||||
|
||||
```bash
|
||||
rtk --version
|
||||
rtk gain
|
||||
rtk init --codex --show
|
||||
```
|
||||
@@ -0,0 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>About — Axiom Works</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav>
|
||||
<div class="nav-inner">
|
||||
<a class="nav-logo" href="index.html">
|
||||
<img src="assets/logo.png" alt="Axiom Works">
|
||||
<span>Axiom Works</span>
|
||||
</a>
|
||||
<ul class="nav-links">
|
||||
<li><a href="index.html">Home</a></li>
|
||||
<li><a href="products.html">Products</a></li>
|
||||
<li><a href="about.html" class="active">About</a></li>
|
||||
<li><a href="people.html">Our Team</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="page-header">
|
||||
<h1>About Axiom Works</h1>
|
||||
<p>Founded in 2011. Still here. Still shipping.</p>
|
||||
</div>
|
||||
|
||||
<main class="page">
|
||||
|
||||
<div class="about-block">
|
||||
<div>
|
||||
<h2>Where we started</h2>
|
||||
<p>Axiom Works was founded in 2011 by a small team of operations veterans who were tired of watching mid-size manufacturers paper over process problems with spreadsheets and tribal knowledge. The original product was a rules engine. It was not elegant, but it worked.</p>
|
||||
<p>Over the next few years, that rules engine became AxiomFlow — a full workflow automation platform built for the realities of industrial operations: shift handoffs, exception handling, equipment downtime, and the kind of edge cases that enterprise vendors prefer not to demo.</p>
|
||||
<p>We have been profitable since 2014. We have not taken outside investment. This is a deliberate choice.</p>
|
||||
|
||||
<div class="stat-row">
|
||||
<div class="stat">
|
||||
<span class="stat-num">2011</span>
|
||||
<span class="stat-lbl">Founded</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-num">280</span>
|
||||
<span class="stat-lbl">Employees</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-num">140+</span>
|
||||
<span class="stat-lbl">Customers</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>What we actually do</h2>
|
||||
<p>AxiomFlow automates the workflows that keep operations running — purchase approvals, quality checks, shift reports, compliance sign-offs, exception escalations. The kind of work that gets done in every facility but rarely appears in a vendor's use-case library.</p>
|
||||
<p>Our customers are mostly mid-size manufacturers and logistics companies in the 200–2,000 employee range. They have real IT departments and real process complexity. They don't need a product designed for a 12-person SaaS startup.</p>
|
||||
<p>We sell to operations leaders and implement with their IT teams. We do not use resellers. When something needs to be configured, a person from Axiom Works handles it.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="divider">
|
||||
|
||||
<section style="margin-bottom: 3.5rem;">
|
||||
<p class="section-label">What We Believe</p>
|
||||
<h2 class="section-title">How we work</h2>
|
||||
<p class="section-intro">These aren't values we arrived at in a workshop. They're conclusions from fourteen years of watching what works and what doesn't.</p>
|
||||
|
||||
<div class="about-block" style="margin-bottom: 0;">
|
||||
<div>
|
||||
<ul class="values-list">
|
||||
<li>
|
||||
<strong>Reliability over features</strong>
|
||||
<span>A workflow automation platform that goes down during a shift is worse than no platform at all. Uptime is not a selling point. It is the baseline.</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Customers are not case studies</strong>
|
||||
<span>We do not publish customer names without permission. We do not write up their implementations as thought leadership. Their problems are not content.</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Slow is smooth, smooth is fast</strong>
|
||||
<span>We have never shipped a feature to meet a conference deadline. This is probably why our release notes are boring and our customers don't have to roll back.</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<ul class="values-list">
|
||||
<li>
|
||||
<strong>Support is not a department</strong>
|
||||
<span>Every customer has a named contact. Support issues get routed to the people who built the feature. The alternative is faster for us and worse for everyone.</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Honest pricing</strong>
|
||||
<span>Our pricing is published. We do not have tiers designed to make the middle option look reasonable. The contract you sign is the contract you get.</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong>We stay in our lane</strong>
|
||||
<span>We automate workflows for operations teams. We are not building an AI platform, a marketplace, or a suite of adjacent products nobody asked for.</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="divider">
|
||||
|
||||
<section id="contact" style="margin-bottom: 2rem;">
|
||||
<p class="section-label">Get in Touch</p>
|
||||
<h2 class="section-title">Talk to us</h2>
|
||||
<p class="section-intro">We respond to every inquiry. Usually the same day, always within 24 hours.</p>
|
||||
|
||||
<div class="card-grid" style="grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));">
|
||||
<div class="card">
|
||||
<div class="card-icon">💬</div>
|
||||
<h3>Sales Inquiries</h3>
|
||||
<p>If you'd like to see a demo or talk through whether AxiomFlow is a fit for your operation, reach out to our sales team.</p>
|
||||
<p style="margin-top:1rem;"><a href="mailto:sales@axiomworks.com">sales@axiomworks.com</a></p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🛠️</div>
|
||||
<h3>Customer Support</h3>
|
||||
<p>Existing customers can reach support directly. Your account contact's email is in your onboarding documentation.</p>
|
||||
<p style="margin-top:1rem;"><a href="mailto:support@axiomworks.com">support@axiomworks.com</a></p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🏢</div>
|
||||
<h3>Our Office</h3>
|
||||
<p>We're headquartered downtown, or close enough to it that we say downtown and nobody pushes back.</p>
|
||||
<p style="margin-top:1rem; color: var(--muted); font-size:0.9rem;">Axiom Works, Inc.<br>Downtown-Adjacent, Suite 300</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="cta-banner">
|
||||
<h2>Meet the team behind the platform</h2>
|
||||
<p>The people who build and support AxiomFlow have been doing this for a while. Some of them have been here since the rules engine days.</p>
|
||||
<a href="people.html" class="btn btn-primary">Our Team</a>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="footer-logo">
|
||||
<img src="assets/logo.png" alt="Axiom Works">
|
||||
<span>Axiom Works</span>
|
||||
</div>
|
||||
<p>© 2025 Axiom Works, Inc. All rights reserved. · <a href="about.html">About</a> · <a href="products.html">Products</a> · <a href="people.html">Our Team</a></p>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 844 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
@@ -0,0 +1,142 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Axiom Works — Workflow Automation for Modern Operations</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav>
|
||||
<div class="nav-inner">
|
||||
<a class="nav-logo" href="index.html">
|
||||
<img src="assets/logo.png" alt="Axiom Works">
|
||||
<span>Axiom Works</span>
|
||||
</a>
|
||||
<ul class="nav-links">
|
||||
<li><a href="index.html" class="active">Home</a></li>
|
||||
<li><a href="products.html">Products</a></li>
|
||||
<li><a href="about.html">About</a></li>
|
||||
<li><a href="people.html">Our Team</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<section class="hero">
|
||||
<div class="hero-inner">
|
||||
<p class="hero-eyebrow">Enterprise Workflow Automation</p>
|
||||
<h1>Streamline. Scale. Succeed.</h1>
|
||||
<p>Axiom Works helps mid-size manufacturers and logistics companies automate the workflows that keep operations running — without the complexity that gets in the way.</p>
|
||||
<div>
|
||||
<a href="products.html" class="btn btn-primary">See Our Products</a>
|
||||
<a href="about.html" class="btn btn-outline">Learn About Us</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<main class="page">
|
||||
|
||||
<section>
|
||||
<p class="section-label">Why Axiom Works</p>
|
||||
<h2 class="section-title">Built for the way operations actually work</h2>
|
||||
<p class="section-intro">Most workflow tools are designed for software teams. AxiomFlow was designed for the people running shifts, managing fleets, and keeping production lines moving.</p>
|
||||
|
||||
<div class="card-grid">
|
||||
<div class="card">
|
||||
<div class="card-icon">⚙️</div>
|
||||
<h3>Configurable Without Consultants</h3>
|
||||
<p>Your team can build and modify workflows without waiting on a vendor or a dedicated IT project. If you can describe the process, you can automate it.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">📊</div>
|
||||
<h3>Visibility Across the Operation</h3>
|
||||
<p>AxiomDash gives managers a live view of what's moving, what's stalled, and where the bottlenecks are — without pulling reports by hand.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🔗</div>
|
||||
<h3>Connects to What You Have</h3>
|
||||
<p>We integrate with ERP systems, warehouse management tools, and the spreadsheets your team has been using since 2009. We don't ask you to start over.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🛡️</div>
|
||||
<h3>Supported by a Real Team</h3>
|
||||
<p>Every customer gets a named support contact. When something breaks during a shift, you call a person — not a ticketing system that routes you to a chatbot.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="section-divider">
|
||||
|
||||
<section>
|
||||
<p class="section-label">Our Platform</p>
|
||||
<h2 class="section-title">The AxiomFlow platform</h2>
|
||||
<p class="section-intro">A connected suite of tools for workflow automation, reporting, and system integration — built to run reliably at scale.</p>
|
||||
|
||||
<div class="card-grid">
|
||||
<div class="card">
|
||||
<div class="card-icon">🔄</div>
|
||||
<h3>AxiomFlow</h3>
|
||||
<p>The core workflow automation platform. Define processes, assign tasks, set triggers, and track completion — all in one place. Trusted by over 140 customers across manufacturing, logistics, and distribution.</p>
|
||||
<span class="card-tag">Flagship Product</span>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">📈</div>
|
||||
<h3>AxiomDash</h3>
|
||||
<p>Real-time reporting and analytics built on top of your AxiomFlow data. Track KPIs, spot trends, and share dashboards with stakeholders — without an analyst in the loop.</p>
|
||||
<span class="card-tag">Analytics Add-On</span>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🔌</div>
|
||||
<h3>AxiomSync</h3>
|
||||
<p>Our legacy data integration layer, connecting older systems to AxiomFlow where modern connectors aren't available. Available for existing customers on legacy contracts.</p>
|
||||
<span class="card-tag legacy">Legacy</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="section-divider">
|
||||
|
||||
<section>
|
||||
<p class="section-label">By The Numbers</p>
|
||||
<h2 class="section-title">A track record that holds up</h2>
|
||||
<p class="section-intro">We've been doing this since 2011. The numbers reflect what happens when you focus on one thing and keep doing it well.</p>
|
||||
|
||||
<div class="card-grid" style="grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));">
|
||||
<div class="card" style="text-align:center;">
|
||||
<span class="stat-num">140+</span>
|
||||
<span class="stat-lbl">Active Customers</span>
|
||||
</div>
|
||||
<div class="card" style="text-align:center;">
|
||||
<span class="stat-num">14</span>
|
||||
<span class="stat-lbl">Years in Business</span>
|
||||
</div>
|
||||
<div class="card" style="text-align:center;">
|
||||
<span class="stat-num">280</span>
|
||||
<span class="stat-lbl">Employees</span>
|
||||
</div>
|
||||
<div class="card" style="text-align:center;">
|
||||
<span class="stat-num">99.6%</span>
|
||||
<span class="stat-lbl">Uptime (12-Month Avg)</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="cta-banner">
|
||||
<h2>Ready to see AxiomFlow in action?</h2>
|
||||
<p>Schedule a 30-minute demo with one of our solutions engineers. No slides, no pitch deck — just the product.</p>
|
||||
<a href="about.html#contact" class="btn btn-primary">Request a Demo</a>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="footer-logo">
|
||||
<img src="assets/logo.png" alt="Axiom Works">
|
||||
<span>Axiom Works</span>
|
||||
</div>
|
||||
<p>© 2025 Axiom Works, Inc. All rights reserved. · <a href="about.html">About</a> · <a href="products.html">Products</a> · <a href="people.html">Our Team</a></p>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,385 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Our Team — Axiom Works</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<style>
|
||||
.exec-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.exec-card {
|
||||
background: var(--white);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r);
|
||||
padding: 2rem 1.75rem;
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.exec-photo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 3px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.exec-initial {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background: var(--navy);
|
||||
color: #fff;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.exec-info h3 { font-size: 1.1rem; font-weight: 600; color: var(--navy-dk); margin-bottom: 0.2rem; }
|
||||
.exec-info .title { font-size: 0.82rem; color: var(--blue); font-weight: 500; margin-bottom: 0.6rem; }
|
||||
.exec-info p { font-size: 0.85rem; color: var(--muted); line-height: 1.55; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav>
|
||||
<div class="nav-inner">
|
||||
<a class="nav-logo" href="index.html">
|
||||
<img src="assets/logo.png" alt="Axiom Works">
|
||||
<span>Axiom Works</span>
|
||||
</a>
|
||||
<ul class="nav-links">
|
||||
<li><a href="index.html">Home</a></li>
|
||||
<li><a href="products.html">Products</a></li>
|
||||
<li><a href="about.html">About</a></li>
|
||||
<li><a href="people.html" class="active">Our Team</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="page-header">
|
||||
<h1>Our Team</h1>
|
||||
<p>280 people. Most of them are not on this page.</p>
|
||||
</div>
|
||||
|
||||
<main class="page">
|
||||
|
||||
<!-- ── EXECUTIVE LEADERSHIP ───────────────────── -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<p class="section-label">Executive Leadership</p>
|
||||
<h2 class="section-title">The people running the company</h2>
|
||||
<p class="section-intro">Axiom Works has been founder-led since 2011. The leadership team is small and has been largely stable since 2015.</p>
|
||||
|
||||
<div class="exec-grid">
|
||||
|
||||
<div class="exec-card">
|
||||
<img class="exec-photo" src="assets/ellen_marsh_ceo_cofounder.png" alt="Ellen Marsh" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
||||
<div class="exec-initial" style="display:none;">EM</div>
|
||||
<div class="exec-info">
|
||||
<h3>Ellen Marsh</h3>
|
||||
<p class="title">CEO & Co-Founder</p>
|
||||
<p>Ellen built the first version of AxiomFlow's rules engine after a decade running operations at a mid-size manufacturer and deciding the tools available were not good enough. She has no CS background, which is probably why the product ended up designed for people who don't either. Attends all-hands twice a year. Has final say on pricing and customer commitments. Does not use Slack.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="exec-card">
|
||||
<img class="exec-photo" src="assets/david_park_cto_cofounder.png" alt="David Park" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
||||
<div class="exec-initial" style="display:none;">DP</div>
|
||||
<div class="exec-info">
|
||||
<h3>David Park</h3>
|
||||
<p class="title">CTO & Co-Founder</p>
|
||||
<p>Wrote the original rules engine in 2011 and has been quietly refactoring it ever since. David now manages engineering managers rather than engineers, which he describes as an acceptable trade. Reviews architecture decisions. Still has opinions about the data model. Has a standing Thursday meeting with security that hasn't moved since 2017.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="exec-card">
|
||||
<img class="exec-photo" src="assets/karen_volkov_coo.png" alt="Karen Volkov" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
||||
<div class="exec-initial" style="display:none;">KV</div>
|
||||
<div class="exec-info">
|
||||
<h3>Karen Volkov</h3>
|
||||
<p class="title">Chief Operating Officer</p>
|
||||
<p>Joined in 2014 to turn a functional startup into a company that could scale past 50 people. Responsible for the fact that Axiom Works has documented processes for anything at all. Has opinions about infrastructure costs that occasionally surface in IT's world via Finance. Prefers decisions with clear owners and deadlines.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="exec-card">
|
||||
<img class="exec-photo" src="assets/rachel_brandt_cfo.png" alt="Rachel Brandt" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
||||
<div class="exec-initial" style="display:none;">RB</div>
|
||||
<div class="exec-info">
|
||||
<h3>Rachel Brandt</h3>
|
||||
<p class="title">Chief Financial Officer</p>
|
||||
<p>Joined in 2016 from a regional accounting firm that handled several of Axiom Works' early customers. Has been working to consolidate the company's cloud spend since 2019. Methodical. Approves all capital expenditure over $5,000. Does not enjoy surprises in the infrastructure budget.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="divider">
|
||||
|
||||
<!-- ── SALES & CUSTOMER SUCCESS ──────────────── -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<p class="section-label">Sales & Customer Success</p>
|
||||
<h2 class="section-title">Getting customers and keeping them</h2>
|
||||
<p class="section-intro">Axiom Works does not use resellers. Every customer relationship runs through this team.</p>
|
||||
|
||||
<div class="people-grid">
|
||||
|
||||
<div class="person-card">
|
||||
<img class="person-photo" src="assets/phil_ruiz_vp_sales.png" alt="Phil Ruiz" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
||||
<div class="person-initial" style="display:none;">PR</div>
|
||||
<h3>Phil Ruiz</h3>
|
||||
<p class="title">VP of Sales</p>
|
||||
<p>Has been promising features to prospects since 2016. Maintains a warm relationship with infrastructure because Marcus once fixed the staging environment with twenty minutes to spare before a demo. Travels frequently. Expense reports submitted promptly.</p>
|
||||
</div>
|
||||
|
||||
<div class="person-card">
|
||||
<img class="person-photo" src="assets/tanya_okafor_head_customer_success.png" alt="Tanya Okafor" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
||||
<div class="person-initial" style="display:none;">TO</div>
|
||||
<h3>Tanya Okafor</h3>
|
||||
<p class="title">Head of Customer Success</p>
|
||||
<p>Manages post-sale relationships for all AxiomFlow customers and the twelve AxiomSync accounts that haven't migrated yet. Uses the word "partnership" a lot. Usually the first person to know when something is wrong in production, because a customer has already called her.</p>
|
||||
</div>
|
||||
|
||||
<div class="person-card">
|
||||
<img class="person-photo" src="assets/mike_kawamoto_account_executive.png" alt="Mike Kawamoto" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
||||
<div class="person-initial" style="display:none;">MK</div>
|
||||
<h3>Mike Kawamoto</h3>
|
||||
<p class="title">Account Executive</p>
|
||||
<p>Handles mid-market manufacturing accounts in the northeast. Has closed more deals in Q4 than any other quarter for four years running. Believes strongly in the demo environment.</p>
|
||||
</div>
|
||||
|
||||
<div class="person-card">
|
||||
<img class="person-photo" src="assets/lisa_ferreira_customer_success_manager.png" alt="Lisa Ferreira" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
||||
<div class="person-initial" style="display:none;">LF</div>
|
||||
<h3>Lisa Ferreira</h3>
|
||||
<p class="title">Customer Success Manager</p>
|
||||
<p>Manages onboarding for new AxiomFlow deployments. Responsible for the onboarding documentation that actually gets used, as opposed to the documentation that exists. Has a talent for figuring out what customers mean rather than what they say.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="divider">
|
||||
|
||||
<!-- ── PRODUCT ───────────────────────────────── -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<p class="section-label">Product</p>
|
||||
<h2 class="section-title">What we build and why</h2>
|
||||
<p class="section-intro">The product team defines the roadmap and answers for it when the roadmap turns out to be wrong.</p>
|
||||
|
||||
<div class="people-grid">
|
||||
|
||||
<div class="person-card">
|
||||
<img class="person-photo" src="assets/sarah-chen.png" alt="Sarah Chen" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
||||
<div class="person-initial" style="display:none;">SC</div>
|
||||
<h3>Sarah Chen</h3>
|
||||
<p class="title">Product Manager, AxiomFlow</p>
|
||||
<p>Owns the AxiomFlow roadmap. Coordinates between sales, engineering, and customers to decide what gets built and in what order. Has strong feelings about the demo environment because it's the product she can see. Emails Monday mornings.</p>
|
||||
</div>
|
||||
|
||||
<div class="person-card">
|
||||
<img class="person-photo" src="assets/ben_portillo_product_manager_axiomdash.png" alt="Ben Portillo" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
||||
<div class="person-initial" style="display:none;">BP</div>
|
||||
<h3>Ben Portillo</h3>
|
||||
<p class="title">Product Manager, AxiomDash</p>
|
||||
<p>Leads product development for the analytics add-on. Works closely with the largest accounts to understand what they actually want from dashboards, which is usually different from what they asked for.</p>
|
||||
</div>
|
||||
|
||||
<div class="person-card">
|
||||
<img class="person-photo" src="assets/annika_gosse_ux_designer.png" alt="Annika Gosse" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
||||
<div class="person-initial" style="display:none;">AG</div>
|
||||
<h3>Annika Gosse</h3>
|
||||
<p class="title">UX Designer</p>
|
||||
<p>Responsible for AxiomFlow's interface layer. Has been advocating for a redesign of the workflow builder since 2022. Produces research that is read carefully and then partially implemented. Patient.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="divider">
|
||||
|
||||
<!-- ── ENGINEERING ───────────────────────────── -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<p class="section-label">Engineering</p>
|
||||
<h2 class="section-title">The people who build it</h2>
|
||||
<p class="section-intro">The engineering team is distributed across product development, integrations, and platform reliability.</p>
|
||||
|
||||
<div class="people-grid">
|
||||
|
||||
<div class="person-card">
|
||||
<img class="person-photo" src="assets/yusuf_halabi_engineering_manager.png" alt="Yusuf Halabi" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
||||
<div class="person-initial" style="display:none;">YH</div>
|
||||
<h3>Yusuf Halabi</h3>
|
||||
<p class="title">Engineering Manager</p>
|
||||
<p>Reports to the CTO and manages the core AxiomFlow platform team. Has opinions about test coverage. Occasionally leaves pull request comments that are technically correct and diplomatically suboptimal. Runs the Thursday architecture review.</p>
|
||||
</div>
|
||||
|
||||
<div class="person-card">
|
||||
<img class="person-photo" src="assets/mei_lin_senior_software_engineer.png" alt="Mei Lin" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
||||
<div class="person-initial" style="display:none;">ML</div>
|
||||
<h3>Mei Lin</h3>
|
||||
<p class="title">Senior Software Engineer</p>
|
||||
<p>Has maintained AxiomSync's integration layer since 2018. Knows more about it than anyone would prefer, including herself. Currently leading the migration tooling project to help the remaining AxiomSync customers off the platform. Thorough commit messages.</p>
|
||||
</div>
|
||||
|
||||
<div class="person-card">
|
||||
<img class="person-photo" src="assets/cora_reyes_software_engineer.png" alt="Cora Reyes" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
||||
<div class="person-initial" style="display:none;">CR</div>
|
||||
<h3>Cora Reyes</h3>
|
||||
<p class="title">Software Engineer</p>
|
||||
<p>Works on the AxiomDash reporting pipeline. Joined in 2022 as a mid-level hire and has been moving steadily toward senior. Has submitted more internal RFCs than anyone else on the team in the past year.</p>
|
||||
</div>
|
||||
|
||||
<div class="person-card">
|
||||
<img class="person-photo" src="assets/nikhil_sharma_platform_engineer.png" alt="Nikhil Sharma" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
||||
<div class="person-initial" style="display:none;">NS</div>
|
||||
<h3>Nikhil Sharma</h3>
|
||||
<p class="title">Platform Engineer</p>
|
||||
<p>Owns the build and release pipeline, the internal CI infrastructure, and the parts of the deployment process that nobody else wants to think about. Has strong opinions about reproducible builds. Occasionally sends Slack messages at 6am.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="divider">
|
||||
|
||||
<!-- ── IT OPERATIONS ─────────────────────────── -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<p class="section-label">IT & Infrastructure</p>
|
||||
<h2 class="section-title">Keeping everything running</h2>
|
||||
<p class="section-intro">The team that manages internal systems, the hosted demo environments, and the infrastructure that everything else depends on.</p>
|
||||
|
||||
<div class="people-grid">
|
||||
|
||||
<div class="person-card">
|
||||
<img class="person-photo" src="assets/dave-kowalski.png" alt="Dave Kowalski" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
||||
<div class="person-initial" style="display:none;">DK</div>
|
||||
<h3>Dave Kowalski</h3>
|
||||
<p class="title">Director of IT Operations</p>
|
||||
<p>Oversees systems, networking, and IT support. Background is originally in network engineering. Has been with Axiom Works since 2015. Describes the infrastructure as mature. Has said "we should really document that" more times than he would admit.</p>
|
||||
</div>
|
||||
|
||||
<div class="person-card">
|
||||
<img class="person-photo" src="assets/marcus-webb.png" alt="Marcus Webb" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
||||
<div class="person-initial" style="display:none;">MW</div>
|
||||
<h3>Marcus Webb</h3>
|
||||
<p class="title">Senior Systems Administrator</p>
|
||||
<p>Six years at Axiom Works. Knows where everything is and why it's there. Communicates efficiently. Available on Slack during business hours and occasionally at 11pm when something is on his mind.</p>
|
||||
</div>
|
||||
|
||||
<div class="person-card">
|
||||
<img class="person-photo" src="assets/rachel_huang_systems_administrator.png" alt="Rachel Huang" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
||||
<div class="person-initial" style="display:none;">RH</div>
|
||||
<h3>Rachel Huang</h3>
|
||||
<p class="title">Systems Administrator</p>
|
||||
<p>Handles provisioning, patch cycles, and the ongoing negotiation with finance over cloud consolidation. Came from a managed services background. Has strong opinions about monitoring dashboards, most of which are correct.</p>
|
||||
</div>
|
||||
|
||||
<div class="person-card">
|
||||
<img class="person-photo" src="assets/tom_malaney_network_engineer.png" alt="Tom Malaney" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
||||
<div class="person-initial" style="display:none;">TM</div>
|
||||
<h3>Tom Malaney</h3>
|
||||
<p class="title">Network Engineer</p>
|
||||
<p>Responsible for network infrastructure across the office and the hosted environments. Has been on-call for more holiday weekends than he prefers to discuss. Thorough in documentation when he finds time to write it.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="divider">
|
||||
|
||||
<!-- ── SECURITY & COMPLIANCE ─────────────────── -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<p class="section-label">Security & Compliance</p>
|
||||
<h2 class="section-title">Risk, access, and the things that matter when they go wrong</h2>
|
||||
<p class="section-intro">Security at Axiom Works is treated as a function, not a checkbox.</p>
|
||||
|
||||
<div class="people-grid">
|
||||
|
||||
<div class="person-card">
|
||||
<img class="person-photo" src="assets/priya-nair.png" alt="Priya Nair" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
||||
<div class="person-initial" style="display:none;">PN</div>
|
||||
<h3>Priya Nair</h3>
|
||||
<p class="title">Head of Security & Compliance</p>
|
||||
<p>Leads all security reviews, access audits, and compliance programmes. Frames concerns in terms of what happens when things go wrong, rather than whether they will. Usually correct. Not someone who appreciates being told about a change after it's already in production.</p>
|
||||
</div>
|
||||
|
||||
<div class="person-card">
|
||||
<img class="person-photo" src="assets/james_osei_security_analyst.png" alt="James Osei" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
||||
<div class="person-initial" style="display:none;">JO</div>
|
||||
<h3>James Osei</h3>
|
||||
<p class="title">Security Analyst</p>
|
||||
<p>Handles vulnerability assessments, access reviews, and quarterly compliance reporting. Methodical. Has a spreadsheet for everything, which is not a criticism.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="divider">
|
||||
|
||||
<!-- ── FINANCE & ADMINISTRATION ──────────────── -->
|
||||
<section style="margin-bottom: 3rem;">
|
||||
<p class="section-label">Finance & Administration</p>
|
||||
<h2 class="section-title">The numbers and the people who manage them</h2>
|
||||
<p class="section-intro">A small team that keeps the books, manages the office, and appears on CC lines of emails that involve infrastructure spending.</p>
|
||||
|
||||
<div class="people-grid">
|
||||
|
||||
<div class="person-card">
|
||||
<img class="person-photo" src="assets/derek_ashford_financial_controller.png" alt="Derek Ashford" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
||||
<div class="person-initial" style="display:none;">DA</div>
|
||||
<h3>Derek Ashford</h3>
|
||||
<p class="title">Financial Controller</p>
|
||||
<p>Manages financial reporting, budget tracking, and vendor contracts. Does not appear at team meetings. Does appear on CC lines of any email that mentions cloud costs, hardware procurement, or infrastructure budget. Always replies-all.</p>
|
||||
</div>
|
||||
|
||||
<div class="person-card">
|
||||
<img class="person-photo" src="assets/sandra_wu_hr_manager.png" alt="Sandra Wu" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
||||
<div class="person-initial" style="display:none;">SW</div>
|
||||
<h3>Sandra Wu</h3>
|
||||
<p class="title">HR Manager</p>
|
||||
<p>Manages hiring, onboarding, and employee relations. Has been with Axiom Works since 2016. Responsible for the onboarding process that new employees go through, which is thorough and takes three days. Sends birthday emails on time, every time.</p>
|
||||
</div>
|
||||
|
||||
<div class="person-card">
|
||||
<img class="person-photo" src="assets/owen_blake_office_manager.png" alt="Owen Blake" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';">
|
||||
<div class="person-initial" style="display:none;">OB</div>
|
||||
<h3>Owen Blake</h3>
|
||||
<p class="title">Office Manager</p>
|
||||
<p>Keeps the office running. Manages facilities, supplies, vendor relationships for non-technical services, and the kitchen situation. Has fixed more things than his job title implies. The person you contact if the conference room equipment stops working.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="cta-banner">
|
||||
<h2>We're hiring — carefully</h2>
|
||||
<p>We add people slowly and try to keep them. Open roles are listed on our careers page. We don't use recruiters.</p>
|
||||
<a href="about.html#contact" class="btn btn-primary">Get in Touch</a>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="footer-logo">
|
||||
<img src="assets/logo.png" alt="Axiom Works">
|
||||
<span>Axiom Works</span>
|
||||
</div>
|
||||
<p>© 2025 Axiom Works, Inc. All rights reserved. · <a href="about.html">About</a> · <a href="products.html">Products</a> · <a href="people.html">Our Team</a></p>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,150 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Products — Axiom Works</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav>
|
||||
<div class="nav-inner">
|
||||
<a class="nav-logo" href="index.html">
|
||||
<img src="assets/logo.png" alt="Axiom Works">
|
||||
<span>Axiom Works</span>
|
||||
</a>
|
||||
<ul class="nav-links">
|
||||
<li><a href="index.html">Home</a></li>
|
||||
<li><a href="products.html" class="active">Products</a></li>
|
||||
<li><a href="about.html">About</a></li>
|
||||
<li><a href="people.html">Our Team</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="page-header">
|
||||
<h1>Products</h1>
|
||||
<p>The AxiomFlow platform and the tools built around it.</p>
|
||||
</div>
|
||||
|
||||
<main class="page">
|
||||
|
||||
<section style="margin-bottom: 1rem;">
|
||||
<p class="section-label">Platform Overview</p>
|
||||
<h2 class="section-title">One platform, built for operations</h2>
|
||||
<p class="section-intro">AxiomFlow is the core. AxiomDash extends it with analytics. AxiomSync bridges older systems where needed. All three are designed to run together — or independently, where that's what makes sense.</p>
|
||||
</section>
|
||||
|
||||
<!-- AxiomFlow -->
|
||||
<div class="product-feature">
|
||||
<div>
|
||||
<span class="product-badge main">Flagship</span>
|
||||
<h3>AxiomFlow</h3>
|
||||
<p>Workflow automation platform for mid-size manufacturers, logistics providers, and distribution operations. Define, deploy, and monitor business processes without a development team or a multi-quarter implementation project.</p>
|
||||
<p>AxiomFlow handles the workflows that matter most: approvals, task routing, exception handling, compliance sign-offs, shift handoffs, and the dozens of other processes that run every day and fail quietly when something goes wrong.</p>
|
||||
<p>Currently active across 140+ customers. Most deployments are live within 60–90 days of contract signing.</p>
|
||||
</div>
|
||||
<div>
|
||||
<ul class="feature-list">
|
||||
<li>Visual workflow builder — no code required for standard processes</li>
|
||||
<li>Role-based task routing with fallback escalation rules</li>
|
||||
<li>Trigger-based automation: time, event, threshold, or external webhook</li>
|
||||
<li>Audit trail on every workflow action — immutable, exportable</li>
|
||||
<li>Exception handling with configurable escalation paths</li>
|
||||
<li>Shift and calendar-aware scheduling</li>
|
||||
<li>ERP and WMS integration via REST API and native connectors</li>
|
||||
<li>Single-sign-on (SAML 2.0, OIDC)</li>
|
||||
<li>On-premise or private cloud deployment</li>
|
||||
<li>99.6% uptime SLA (12-month rolling average)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AxiomDash -->
|
||||
<div class="product-feature">
|
||||
<div>
|
||||
<span class="product-badge">Analytics Add-On</span>
|
||||
<h3>AxiomDash</h3>
|
||||
<p>Reporting and analytics built directly on top of your AxiomFlow data. No ETL pipeline, no separate database, no BI tool license to negotiate. If it happened in AxiomFlow, AxiomDash can show it.</p>
|
||||
<p>Designed for operations managers and team leads who need a live view of what's moving, what's overdue, and where the recurring problems are — without pulling reports by hand or waiting on an analyst.</p>
|
||||
</div>
|
||||
<div>
|
||||
<ul class="feature-list">
|
||||
<li>Pre-built dashboards for common operational metrics</li>
|
||||
<li>Custom dashboard builder with drag-and-drop layout</li>
|
||||
<li>Live data — no scheduled refresh, no stale snapshots</li>
|
||||
<li>Threshold alerts via email or webhook</li>
|
||||
<li>Shareable read-only views for stakeholders without platform access</li>
|
||||
<li>Export to CSV, PDF, or scheduled email delivery</li>
|
||||
<li>Role-based visibility — teams see their data, not each other's</li>
|
||||
<li>Available as an add-on to any AxiomFlow subscription</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AxiomSync -->
|
||||
<div class="product-feature legacy">
|
||||
<div>
|
||||
<span class="product-badge end-of-sale">End of Sale</span>
|
||||
<h3>AxiomSync</h3>
|
||||
<p>Legacy data integration layer for connecting older systems — primarily pre-2015 ERP installations and proprietary shop-floor software — to AxiomFlow where modern API connectors aren't available.</p>
|
||||
<p>AxiomSync has been end-of-sale since 2021. It remains in active maintenance for existing customers on legacy contracts. No new deployments are supported.</p>
|
||||
<p>Customers still on AxiomSync are encouraged to discuss migration options with their account contact. The migration path to native AxiomFlow connectors is well-documented and typically takes one to two quarters depending on integration complexity.</p>
|
||||
</div>
|
||||
<div>
|
||||
<ul class="feature-list">
|
||||
<li>File-based and database-level integration for legacy systems</li>
|
||||
<li>Scheduled sync jobs with configurable polling intervals</li>
|
||||
<li>Transform and mapping layer for data normalization</li>
|
||||
<li>Error logging and alerting for failed sync events</li>
|
||||
<li>Maintained for existing customers through end of current contract terms</li>
|
||||
</ul>
|
||||
<p style="margin-top: 1.5rem; font-size: 0.85rem; color: var(--muted);">If you are an AxiomSync customer and have questions about your contract or migration timeline, contact your account representative directly.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="section-divider">
|
||||
|
||||
<section style="margin-bottom: 2rem;">
|
||||
<p class="section-label">Deployment</p>
|
||||
<h2 class="section-title">How it runs</h2>
|
||||
<p class="section-intro">AxiomFlow is designed to run in your environment — not ours. We support private cloud and on-premise deployments for customers with data residency or security requirements that preclude multi-tenant SaaS.</p>
|
||||
|
||||
<div class="card-grid">
|
||||
<div class="card">
|
||||
<div class="card-icon">🏗️</div>
|
||||
<h3>On-Premise</h3>
|
||||
<p>Full installation in your data centre. You own the stack. We provide the software, the documentation, and the support. Suitable for customers with strict data residency requirements or existing on-prem infrastructure.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">☁️</div>
|
||||
<h3>Private Cloud</h3>
|
||||
<p>Deployed in your cloud tenancy (AWS, Azure, or GCP). Single-tenant. Your VPC, your keys, your audit logs. We handle the application layer; you retain control of the infrastructure.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">🤝</div>
|
||||
<h3>Managed Hosting</h3>
|
||||
<p>For customers who want the isolation of a private deployment without the operational overhead, we offer managed single-tenant hosting in our infrastructure. Contact sales for availability.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="cta-banner">
|
||||
<h2>See it running in your scenario</h2>
|
||||
<p>We'll walk through a demo built around your actual processes, not a generic workflow that happens to look impressive on a projector.</p>
|
||||
<a href="about.html#contact" class="btn btn-primary">Request a Demo</a>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<div class="footer-logo">
|
||||
<img src="assets/logo.png" alt="Axiom Works">
|
||||
<span>Axiom Works</span>
|
||||
</div>
|
||||
<p>© 2025 Axiom Works, Inc. All rights reserved. · <a href="about.html">About</a> · <a href="products.html">Products</a> · <a href="people.html">Our Team</a></p>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,377 @@
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--navy: #1b3558;
|
||||
--navy-dk: #112240;
|
||||
--blue: #2563a0;
|
||||
--blue-lt: #3b82c4;
|
||||
--bg: #f4f6f9;
|
||||
--white: #ffffff;
|
||||
--text: #1a1f2e;
|
||||
--muted: #6b7280;
|
||||
--border: #d1d9e6;
|
||||
--shadow: 0 1px 4px rgba(0,0,0,.10), 0 4px 16px rgba(0,0,0,.06);
|
||||
--r: 6px;
|
||||
--max: 1100px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
a { color: var(--blue); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
img { display: block; max-width: 100%; }
|
||||
|
||||
/* ── NAV ─────────────────────────────────────────── */
|
||||
nav {
|
||||
background: var(--navy-dk);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.nav-inner {
|
||||
max-width: var(--max);
|
||||
margin: 0 auto;
|
||||
padding: 0 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2.5rem;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.nav-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-logo img { height: 32px; width: 32px; border-radius: 4px; }
|
||||
|
||||
.nav-logo span {
|
||||
color: #ffffff;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
list-style: none;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
display: block;
|
||||
color: rgba(255,255,255,.78);
|
||||
font-size: 0.9rem;
|
||||
padding: 0 1rem;
|
||||
line-height: 60px;
|
||||
letter-spacing: 0.01em;
|
||||
transition: color .15s, background .15s;
|
||||
}
|
||||
|
||||
.nav-links a:hover,
|
||||
.nav-links a.active { color: #fff; background: rgba(255,255,255,.08); text-decoration: none; }
|
||||
|
||||
/* ── HERO ────────────────────────────────────────── */
|
||||
.hero {
|
||||
background: linear-gradient(135deg, var(--navy-dk) 0%, var(--navy) 55%, var(--blue) 100%);
|
||||
color: #fff;
|
||||
padding: 6rem 1.5rem 5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-inner { max-width: 680px; margin: 0 auto; }
|
||||
|
||||
.hero-eyebrow {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.18em;
|
||||
font-size: 0.8rem;
|
||||
color: rgba(255,255,255,.6);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: clamp(2.2rem, 5vw, 3.4rem);
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 1.15rem;
|
||||
color: rgba(255,255,255,.82);
|
||||
margin-bottom: 2.25rem;
|
||||
max-width: 520px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.8rem 2rem;
|
||||
border-radius: var(--r);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: filter .15s;
|
||||
}
|
||||
|
||||
.btn:hover { filter: brightness(1.1); text-decoration: none; }
|
||||
|
||||
.btn-primary { background: #fff; color: var(--navy); }
|
||||
.btn-outline { background: transparent; color: #fff; border: 2px solid rgba(255,255,255,.5); margin-left: 0.75rem; }
|
||||
|
||||
/* ── PAGE WRAPPER ────────────────────────────────── */
|
||||
.page { max-width: var(--max); margin: 0 auto; padding: 3.5rem 1.5rem 5rem; }
|
||||
|
||||
/* ── SECTION HEADINGS ────────────────────────────── */
|
||||
.section-label {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.14em;
|
||||
font-size: 0.75rem;
|
||||
color: var(--blue);
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h2.section-title {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
color: var(--navy-dk);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.section-intro {
|
||||
color: var(--muted);
|
||||
max-width: 580px;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
/* ── CARDS ───────────────────────────────────────── */
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--white);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r);
|
||||
padding: 1.75rem;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 8px;
|
||||
background: var(--navy);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 1.1rem;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.card h3 { font-size: 1.1rem; font-weight: 600; color: var(--navy-dk); margin-bottom: 0.5rem; }
|
||||
.card p { font-size: 0.9rem; color: var(--muted); line-height: 1.6; }
|
||||
|
||||
.card-tag {
|
||||
display: inline-block;
|
||||
margin-top: 1rem;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
background: #e8f0fb;
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.card-tag.legacy { background: #f0f0f0; color: var(--muted); }
|
||||
|
||||
/* ── PEOPLE GRID ─────────────────────────────────── */
|
||||
.people-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.person-card {
|
||||
background: var(--white);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r);
|
||||
padding: 1.75rem 1.5rem;
|
||||
box-shadow: var(--shadow);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.person-photo {
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
margin: 0 auto 1.1rem;
|
||||
border: 3px solid var(--border);
|
||||
}
|
||||
|
||||
.person-initial {
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
border-radius: 50%;
|
||||
background: var(--navy);
|
||||
color: #fff;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 1.1rem;
|
||||
}
|
||||
|
||||
.person-card h3 { font-size: 1rem; font-weight: 600; color: var(--navy-dk); margin-bottom: 0.25rem; }
|
||||
.person-card .title { font-size: 0.82rem; color: var(--blue); font-weight: 500; margin-bottom: 0.5rem; }
|
||||
.person-card p { font-size: 0.83rem; color: var(--muted); line-height: 1.55; }
|
||||
|
||||
/* ── ABOUT SECTIONS ──────────────────────────────── */
|
||||
.about-block {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 3rem;
|
||||
align-items: start;
|
||||
margin-bottom: 3.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.about-block { grid-template-columns: 1fr; gap: 2rem; }
|
||||
}
|
||||
|
||||
.about-block h2 { font-size: 1.55rem; font-weight: 700; color: var(--navy-dk); margin-bottom: 1rem; }
|
||||
.about-block p { color: var(--muted); margin-bottom: 0.9rem; font-size: 0.95rem; }
|
||||
|
||||
.stat-row { display: flex; gap: 2rem; flex-wrap: wrap; margin-top: 2rem; }
|
||||
|
||||
.stat { text-align: center; }
|
||||
.stat-num { font-size: 2rem; font-weight: 700; color: var(--navy); display: block; }
|
||||
.stat-lbl { font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--muted); }
|
||||
|
||||
.values-list { list-style: none; }
|
||||
.values-list li {
|
||||
padding: 1rem 1.2rem;
|
||||
border-left: 3px solid var(--blue);
|
||||
margin-bottom: 1rem;
|
||||
background: var(--white);
|
||||
border-radius: 0 var(--r) var(--r) 0;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.values-list li strong { display: block; color: var(--navy-dk); margin-bottom: 0.2rem; font-size: 0.95rem; }
|
||||
.values-list li span { font-size: 0.87rem; color: var(--muted); }
|
||||
|
||||
/* ── PRODUCT DETAIL ──────────────────────────────── */
|
||||
.product-feature {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.5fr;
|
||||
gap: 2.5rem;
|
||||
align-items: start;
|
||||
background: var(--white);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r);
|
||||
padding: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.product-feature { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.product-feature.legacy { opacity: 0.7; }
|
||||
|
||||
.product-badge {
|
||||
display: inline-block;
|
||||
padding: 0.35rem 0.9rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
background: #e8f0fb;
|
||||
color: var(--blue);
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.product-badge.main { background: var(--navy); color: #fff; }
|
||||
.product-badge.end-of-sale { background: #f0f0f0; color: var(--muted); }
|
||||
|
||||
.product-feature h3 { font-size: 1.3rem; font-weight: 700; color: var(--navy-dk); margin-bottom: 0.5rem; }
|
||||
.product-feature p { color: var(--muted); font-size: 0.92rem; line-height: 1.6; margin-bottom: 0.9rem; }
|
||||
|
||||
.feature-list { list-style: none; }
|
||||
.feature-list li {
|
||||
font-size: 0.88rem;
|
||||
color: var(--muted);
|
||||
padding: 0.3rem 0;
|
||||
padding-left: 1.2rem;
|
||||
position: relative;
|
||||
}
|
||||
.feature-list li::before { content: "✓"; position: absolute; left: 0; color: var(--blue-lt); font-weight: 700; }
|
||||
|
||||
/* ── BANNER ──────────────────────────────────────── */
|
||||
.cta-banner {
|
||||
background: linear-gradient(135deg, var(--navy-dk), var(--blue));
|
||||
color: #fff;
|
||||
border-radius: var(--r);
|
||||
padding: 3rem 2rem;
|
||||
text-align: center;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.cta-banner h2 { font-size: 1.6rem; margin-bottom: 0.75rem; }
|
||||
.cta-banner p { color: rgba(255,255,255,.75); margin-bottom: 1.5rem; }
|
||||
|
||||
/* ── FOOTER ──────────────────────────────────────── */
|
||||
footer {
|
||||
background: var(--navy-dk);
|
||||
color: rgba(255,255,255,.5);
|
||||
font-size: 0.82rem;
|
||||
text-align: center;
|
||||
padding: 2rem 1.5rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
footer a { color: rgba(255,255,255,.6); }
|
||||
footer .footer-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
footer .footer-logo img { height: 22px; width: 22px; opacity: 0.8; }
|
||||
footer .footer-logo span { color: rgba(255,255,255,.75); font-weight: 600; font-size: 0.9rem; }
|
||||
|
||||
/* ── PAGE HEADER (inner pages) ───────────────────── */
|
||||
.page-header {
|
||||
background: linear-gradient(135deg, var(--navy-dk), var(--navy));
|
||||
color: #fff;
|
||||
padding: 3.5rem 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-header h1 { font-size: clamp(1.8rem, 4vw, 2.6rem); margin-bottom: 0.5rem; }
|
||||
.page-header p { color: rgba(255,255,255,.7); font-size: 1rem; max-width: 540px; margin: 0 auto; }
|
||||
|
||||
/* ── DIVIDER ─────────────────────────────────────── */
|
||||
.divider { border: none; border-top: 1px solid var(--border); margin: 2.5rem 0; }
|
||||
|
||||
hr.section-divider { border: none; border-top: 1px solid var(--border); margin: 3rem 0; }
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"id": "marcus-Q001",
|
||||
"character": "marcus",
|
||||
"quest_id": "Q001",
|
||||
"series_id": "marcus-main",
|
||||
"series_position": 1,
|
||||
"messages": [
|
||||
{
|
||||
"stage": "intro",
|
||||
"trigger": "quest_activated",
|
||||
"body": "The onboarding doc has your key and the path you need. It's in /etc/axiom/onboarding on ares once you're in. Or ask me and I'll paste it here. Either way."
|
||||
},
|
||||
{
|
||||
"stage": "hint_1",
|
||||
"trigger": "player_requested_help",
|
||||
"body": "Start in your home directory. You need a .ssh folder if it does not exist yet. Then authorized_keys inside it."
|
||||
},
|
||||
{
|
||||
"stage": "hint_2",
|
||||
"trigger": "player_requested_help_again",
|
||||
"body": "The permissions matter more than people expect. SSH will silently refuse a key if the file or the directory is group-writable. 700 on the folder, 600 on the file."
|
||||
},
|
||||
{
|
||||
"stage": "hint_3",
|
||||
"trigger": "player_requested_help_again",
|
||||
"body": "mkdir -p ~/.ssh && chmod 700 ~/.ssh. Then echo your public key into ~/.ssh/authorized_keys and chmod 600 that file. That is the whole thing."
|
||||
},
|
||||
{
|
||||
"stage": "complete-clean",
|
||||
"trigger": "world_flag:player_ssh_configured",
|
||||
"body": "Good. You're in. I'll send you the next thing shortly. The coffee machine on this floor is broken, heads up."
|
||||
},
|
||||
{
|
||||
"stage": "complete-permissive",
|
||||
"trigger": "world_flag:player_loose_permissions",
|
||||
"body": "Key's in there. One thing though — check the permissions on that file. SSH is picky about it. Might not bite you today but it will eventually."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"id": "marcus-Q002",
|
||||
"character": "marcus",
|
||||
"quest_id": "Q002",
|
||||
"series_id": "marcus-main",
|
||||
"series_position": 2,
|
||||
"messages": [
|
||||
{
|
||||
"stage": "intro",
|
||||
"trigger": "quest_activated",
|
||||
"body": "Sarah's ticket is real. The site's down. Hermes is the web server — you can SSH from ares. Have a look at what nginx is doing."
|
||||
},
|
||||
{
|
||||
"stage": "hint_1",
|
||||
"trigger": "player_requested_help",
|
||||
"body": "If nginx won't start, it usually tells you why. Try nginx -t before you touch anything else."
|
||||
},
|
||||
{
|
||||
"stage": "hint_2",
|
||||
"trigger": "player_requested_help_again",
|
||||
"body": "Whatever the error says, it will include a file path and a line number. Go look at that exact spot."
|
||||
},
|
||||
{
|
||||
"stage": "hint_3",
|
||||
"trigger": "player_requested_help_again",
|
||||
"body": "Config syntax errors are usually small. Missing semicolons, wrong brackets, typos on directive names. Read it carefully."
|
||||
},
|
||||
{
|
||||
"stage": "complete-clean",
|
||||
"trigger": "world_flag:nginx_stable",
|
||||
"body": "Good. Sarah will see it come back up. Worth checking systemctl is-enabled nginx while you're there — if someone broke the config they may have been poking around other things too."
|
||||
},
|
||||
{
|
||||
"stage": "complete-not-enabled",
|
||||
"trigger": "world_flag:nginx_unstable",
|
||||
"body": "It's running. But if that machine reboots for any reason nginx won't come back up automatically. You might want to fix that before Sarah notices."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"id": "marcus-Q003",
|
||||
"character": "marcus",
|
||||
"quest_id": "Q003",
|
||||
"series_id": "marcus-main",
|
||||
"series_position": 3,
|
||||
"messages": [
|
||||
{
|
||||
"stage": "intro",
|
||||
"trigger": "quest_activated",
|
||||
"body": "Dave's report is vague but something is wrong on hermes. I'd start by looking at resource utilization before assuming it's the application."
|
||||
},
|
||||
{
|
||||
"stage": "hint_1",
|
||||
"trigger": "player_requested_help",
|
||||
"body": "Check disk. df -h is your friend. Web servers write logs constantly and nobody always remembers to set up rotation."
|
||||
},
|
||||
{
|
||||
"stage": "hint_2",
|
||||
"trigger": "player_requested_help_again",
|
||||
"body": "If you find a big file, don't just delete it — figure out why it got that big. Is logrotate configured for nginx? Check /etc/logrotate.d/."
|
||||
},
|
||||
{
|
||||
"stage": "hint_3",
|
||||
"trigger": "player_requested_help_again",
|
||||
"body": "The default nginx logrotate config is in the nginx package. dpkg -L nginx | grep logrotate might give you somewhere to start. Or just write a correct one — it's about ten lines."
|
||||
},
|
||||
{
|
||||
"stage": "complete-clean",
|
||||
"trigger": "world_flag:hermes_logrotate_healthy",
|
||||
"body": "Nice. That was the right call — clearing the space and fixing what caused it. Logrotate problems have a way of coming back if you don't actually fix them."
|
||||
},
|
||||
{
|
||||
"stage": "complete-norotate",
|
||||
"trigger": "world_flag:hermes_log_pressure_pending",
|
||||
"body": "Space is back. But if you didn't fix the rotation config that log is going to grow again. Something to keep an eye on."
|
||||
},
|
||||
{
|
||||
"stage": "complete-down",
|
||||
"trigger": "world_flag:hermes_web_down",
|
||||
"body": "nginx is inactive now? That's worse than the disk problem. Restarting it without fixing why it died isn't a fix, it's a delay. Check what happened before you start it again."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"id": "marcus-Q004",
|
||||
"character": "marcus",
|
||||
"quest_id": "Q004",
|
||||
"series_id": "marcus-main",
|
||||
"series_position": 4,
|
||||
"messages": [
|
||||
{
|
||||
"stage": "intro",
|
||||
"trigger": "quest_activated",
|
||||
"body": "Sarah's deploy thing is interesting. If the script said it ran fine but the files didn't change, something is blocking the write. I'd look at ownership before I touch the script."
|
||||
},
|
||||
{
|
||||
"stage": "hint_1",
|
||||
"trigger": "player_requested_help",
|
||||
"body": "ls -la on the web root. If those files are owned by root and the deploy runs as www-data, that's your problem."
|
||||
},
|
||||
{
|
||||
"stage": "hint_2",
|
||||
"trigger": "player_requested_help_again",
|
||||
"body": "chown. And use -R unless you enjoy doing it twice."
|
||||
},
|
||||
{
|
||||
"stage": "hint_3",
|
||||
"trigger": "player_requested_help_again",
|
||||
"body": "chown -R www-data:www-data /var/www/axiomworks. Then you can trigger the deploy service to confirm it takes."
|
||||
},
|
||||
{
|
||||
"stage": "complete-clean",
|
||||
"trigger": "world_flag:hermes_deploy_healthy",
|
||||
"body": "Good. Someone ran that deploy as root at some point. Worth figuring out who has sudo on hermes and whether they should."
|
||||
},
|
||||
{
|
||||
"stage": "complete-partial",
|
||||
"trigger": "world_flag:hermes_deploy_partial",
|
||||
"body": "Ownership is fixed on the directory but I'm not sure the files inside are correct. Sarah might still hit issues on the next deploy."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"id": "marcus-Q005",
|
||||
"character": "marcus",
|
||||
"quest_id": "Q005",
|
||||
"series_id": "marcus-main",
|
||||
"series_position": 5,
|
||||
"messages": [
|
||||
{
|
||||
"stage": "intro",
|
||||
"trigger": "quest_activated",
|
||||
"body": "Dave's disk alert is on /var/backups this time, not /var/log. That's a different problem. Something to do with the backup job probably."
|
||||
},
|
||||
{
|
||||
"stage": "hint_1",
|
||||
"trigger": "player_requested_help",
|
||||
"body": "Look at what owns the files in that directory. If it's root and the backup agent is supposed to manage them, someone ran something as the wrong user."
|
||||
},
|
||||
{
|
||||
"stage": "hint_2",
|
||||
"trigger": "player_requested_help_again",
|
||||
"body": "Check /etc/cron.d/. Jobs in there can specify a user on the line. If there's no user field it defaults to root."
|
||||
},
|
||||
{
|
||||
"stage": "hint_3",
|
||||
"trigger": "player_requested_help_again",
|
||||
"body": "The line format is: schedule user command. If yours is just: schedule command — that's the problem. Add the user field."
|
||||
},
|
||||
{
|
||||
"stage": "complete-clean",
|
||||
"trigger": "world_flag:hermes_backup_healthy",
|
||||
"body": "Good catch on the ownership cleanup too. A lot of people would have just fixed the cron line and left the old root-owned files sitting there."
|
||||
},
|
||||
{
|
||||
"stage": "complete-partial",
|
||||
"trigger": "world_flag:hermes_backup_partial",
|
||||
"body": "Cron's correct now. The old files are still owned by root though — the retention script won't be able to clean them up. Worth sorting that out before the disk fills again."
|
||||
},
|
||||
{
|
||||
"stage": "complete-wrong",
|
||||
"trigger": "world_flag:hermes_backup_root_running",
|
||||
"body": "Disk's clear. But what was actually running that job? If root is still running it that directory is going to fill up again."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"id": "marcus-Q006",
|
||||
"character": "marcus",
|
||||
"quest_id": "Q006",
|
||||
"series_id": "marcus-main",
|
||||
"series_position": 6,
|
||||
"messages": [
|
||||
{
|
||||
"stage": "intro",
|
||||
"trigger": "quest_activated",
|
||||
"body": "Vulcan is Arch. Different from what you've been working on. Package manager is pacman, not apt. Same concepts, different commands. Signature errors usually mean keyring or clock problems."
|
||||
},
|
||||
{
|
||||
"stage": "hint_1",
|
||||
"trigger": "player_requested_help",
|
||||
"body": "Check what time that machine thinks it is. timedatectl. If NTP isn't running the clock drifts and GPG signatures start looking like they're from the future."
|
||||
},
|
||||
{
|
||||
"stage": "hint_2",
|
||||
"trigger": "player_requested_help_again",
|
||||
"body": "systemctl enable --now systemd-timesyncd. Then wait a moment for sync, and try pacman again. You may also need to refresh the keyring."
|
||||
},
|
||||
{
|
||||
"stage": "hint_3",
|
||||
"trigger": "player_requested_help_again",
|
||||
"body": "pacman -S archlinux-keyring to refresh. Then pacman -Syu should work."
|
||||
},
|
||||
{
|
||||
"stage": "complete-clean",
|
||||
"trigger": "world_flag:vulcan_builds_healthy",
|
||||
"body": "Clock drift breaking pacman is one of those things that seems unrelated until you've seen it twice. You'll spot it immediately next time."
|
||||
},
|
||||
{
|
||||
"stage": "complete-fragile",
|
||||
"trigger": "world_flag:vulcan_ntp_fragile",
|
||||
"body": "Timesyncd is running and builds work. It's not enabled at boot though — worth fixing that so the next reboot doesn't put you back here."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"id": "marcus-Q007",
|
||||
"character": "marcus",
|
||||
"quest_id": "Q007",
|
||||
"series_id": "marcus-main",
|
||||
"series_position": 7,
|
||||
"messages": [
|
||||
{
|
||||
"stage": "intro",
|
||||
"trigger": "quest_activated",
|
||||
"body": "Priya can't get into hermes. Something in the SSH config changed. Figure out what it was and restore her access without creating a new problem."
|
||||
},
|
||||
{
|
||||
"stage": "hint_1",
|
||||
"trigger": "player_requested_help",
|
||||
"body": "sshd_config is where SSH restrictions live. Look for AllowUsers or AllowGroups. One of those is either missing her or was set wrong."
|
||||
},
|
||||
{
|
||||
"stage": "hint_2",
|
||||
"trigger": "player_requested_help_again",
|
||||
"body": "AllowGroups is the right pattern — it scales. AllowUsers is a list you have to maintain manually. Either works, but think about which one you want to be maintaining in six months."
|
||||
},
|
||||
{
|
||||
"stage": "complete-clean",
|
||||
"trigger": "world_flag:hermes_ssh_hardened_correct",
|
||||
"body": "AllowGroups with web-admin. That's the correct way to do it. Users in the group get access, users not in the group don't. No list to maintain."
|
||||
},
|
||||
{
|
||||
"stage": "complete-fragile",
|
||||
"trigger": "world_flag:hermes_ssh_allowusers_fragile",
|
||||
"body": "Priya's back in. That AllowUsers list is going to need a line added every time someone new needs access. Worth switching to group-based before it becomes a problem."
|
||||
},
|
||||
{
|
||||
"stage": "complete-regression",
|
||||
"trigger": "world_flag:hermes_ssh_unrestricted",
|
||||
"body": "Access is restored but the hardening is gone. That restriction was there for a reason — SSH open to everyone on hermes isn't a great position to be in."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"id": "marcus-Q008",
|
||||
"character": "marcus",
|
||||
"quest_id": "Q008",
|
||||
"series_id": "marcus-main",
|
||||
"series_position": 8,
|
||||
"messages": [
|
||||
{
|
||||
"stage": "intro",
|
||||
"trigger": "quest_activated",
|
||||
"body": "App's down after an update. First question is always: what changed. Sarah says a new package version came in. I'd start by looking at whether the binary actually runs."
|
||||
},
|
||||
{
|
||||
"stage": "hint_1",
|
||||
"trigger": "player_requested_help",
|
||||
"body": "journalctl -u axiomworks-app. If it's failing immediately, it's probably the binary itself, not config. Try running it directly and see what the error is."
|
||||
},
|
||||
{
|
||||
"stage": "hint_2",
|
||||
"trigger": "player_requested_help_again",
|
||||
"body": "If the binary is bad, figure out where the package came from. pacman -Qi axiomworks-app will show you the repo. If it's coming from vulcan, go look at what they built."
|
||||
},
|
||||
{
|
||||
"stage": "hint_3",
|
||||
"trigger": "player_requested_help_again",
|
||||
"body": "You can roll back with pacman -U /var/cache/pacman/pkg/ if the old package is still cached. Or go to the repo on vulcan and look for an older version."
|
||||
},
|
||||
{
|
||||
"stage": "complete-rollback",
|
||||
"trigger": "world_flag:hermes_app_pinned_2-1-0",
|
||||
"body": "Solid. Pinning the version means the next update cycle won't pull the broken one back in. Someone needs to fix that build on vulcan at some point though."
|
||||
},
|
||||
{
|
||||
"stage": "complete-unpinned",
|
||||
"trigger": "world_flag:hermes_app_running",
|
||||
"body": "App's running again. Is the version pinned? If not the next pacman -Syu is going to pull 2.1.1 back in and you'll be back here."
|
||||
},
|
||||
{
|
||||
"stage": "complete-rebuild",
|
||||
"trigger": "world_flag:vulcan_build_fixed",
|
||||
"body": "You fixed it at the source. That's the right call if you have time for it. What was wrong with the build?"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"id": "marcus-day-one",
|
||||
"character": "marcus",
|
||||
"quest_id": "",
|
||||
"series_id": "marcus-main",
|
||||
"series_position": 0,
|
||||
"messages": [
|
||||
{
|
||||
"stage": "welcome",
|
||||
"trigger": "immediate",
|
||||
"body": "Welcome. You're replacing Dale. Nobody will tell you what Dale did because it's complicated. Your badge number is pending — Dave from Finance has your temp credentials. He's on three today."
|
||||
},
|
||||
{
|
||||
"stage": "setup",
|
||||
"trigger": "immediate",
|
||||
"body": "Your machine is ares. You'll need to set up SSH keys before anything else will work. I'll send you the first ticket once provisioning clears. Probably this morning."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"id": "priya-Q007-followup",
|
||||
"character": "priya",
|
||||
"quest_id": "Q007",
|
||||
"series_id": "priya-ops",
|
||||
"series_position": 2,
|
||||
"messages": [
|
||||
{
|
||||
"stage": "after-action",
|
||||
"trigger": "world_flag:priya_access_restored",
|
||||
"body": "Access is back. Thank you. I can finish the incident review now without SSH getting in the way."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"id": "priya-Q007",
|
||||
"character": "priya",
|
||||
"quest_id": "Q007",
|
||||
"series_id": "priya-ops",
|
||||
"series_position": 1,
|
||||
"messages": [
|
||||
{
|
||||
"stage": "intro",
|
||||
"trigger": "quest_activated",
|
||||
"body": "I need access to hermes restored. I was in the middle of investigating an error and now I can't get back in. Find out what changed and fix it."
|
||||
},
|
||||
{
|
||||
"stage": "complete-clean",
|
||||
"trigger": "world_flag:hermes_ssh_hardened_correct",
|
||||
"body": "Back in. AllowGroups is the right way to do it — using AllowUsers was going to be a maintenance problem. Good call."
|
||||
},
|
||||
{
|
||||
"stage": "complete-fragile",
|
||||
"trigger": "world_flag:hermes_ssh_allowusers_fragile",
|
||||
"body": "Access restored. That AllowUsers list is going to need updating every time someone new needs access. Might want to switch to group-based at some point."
|
||||
},
|
||||
{
|
||||
"stage": "complete-regression",
|
||||
"trigger": "world_flag:hermes_ssh_unrestricted",
|
||||
"body": "I'm back in. But it looks like all SSH restrictions are gone now. That hardening was probably there for a reason."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"id": "priya-shift-review",
|
||||
"character": "priya",
|
||||
"messages": [
|
||||
{
|
||||
"stage": "excellent",
|
||||
"trigger": "shift_review",
|
||||
"body": "Strong shift. You handled the queue cleanly and did not create extra work for anyone else."
|
||||
},
|
||||
{
|
||||
"stage": "ok",
|
||||
"trigger": "shift_review",
|
||||
"body": "Acceptable shift. The important thing is that the work moved forward and the environment stayed stable."
|
||||
},
|
||||
{
|
||||
"stage": "poor",
|
||||
"trigger": "shift_review",
|
||||
"body": "This shift needs review. Resolve the backlog cleanly next time and stop leaving avoidable mess behind."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"id": "sarah-Q003-angry",
|
||||
"character": "sarah",
|
||||
"quest_id": "Q003",
|
||||
"messages": [
|
||||
{
|
||||
"stage": "nginx-killed",
|
||||
"trigger": "world_flag:hermes_web_down",
|
||||
"body": "The site is completely down now. It was slow before — now it's returning nothing. What happened?"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "sarah-Q004",
|
||||
"character": "sarah",
|
||||
"quest_id": "Q004",
|
||||
"messages": [
|
||||
{
|
||||
"stage": "intro",
|
||||
"trigger": "quest_activated",
|
||||
"body": "My last deploy ran without errors but nothing changed on the site. The script didn't fail, it just... didn't do anything. Files in /var/www are owned by root for some reason."
|
||||
},
|
||||
{
|
||||
"stage": "complete-clean",
|
||||
"trigger": "world_flag:hermes_deploy_healthy",
|
||||
"body": "Deploy's working again. I pushed a test change and it applied. Thanks for sorting the ownership — not sure how that happened but it's fixed now."
|
||||
},
|
||||
{
|
||||
"stage": "complete-partial",
|
||||
"trigger": "world_flag:hermes_deploy_partial",
|
||||
"body": "The top-level directory is writable now but the files inside it still aren't. Next deploy is going to fail on the individual files. Can you finish the ownership fix?"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"id": "sarah-Q008",
|
||||
"character": "sarah",
|
||||
"quest_id": "Q008",
|
||||
"messages": [
|
||||
{
|
||||
"stage": "intro",
|
||||
"trigger": "quest_activated",
|
||||
"body": "The app is crashing immediately after the last update. I didn't push any config changes. It was the package — axiomworks-app 2.1.1 is broken. Whatever vulcan built, it doesn't work."
|
||||
},
|
||||
{
|
||||
"stage": "complete-pinned",
|
||||
"trigger": "world_flag:hermes_app_pinned_2-1-0",
|
||||
"body": "App's running. The apt pin means we won't accidentally pull 2.1.1 in again. Someone needs to sort out what went wrong on vulcan before we can upgrade properly."
|
||||
},
|
||||
{
|
||||
"stage": "complete-rebuilt",
|
||||
"trigger": "world_flag:vulcan_build_fixed",
|
||||
"body": "App's running and the build is fixed. That's the right fix. I was hoping someone would trace it back to the source rather than just rolling back and leaving it."
|
||||
},
|
||||
{
|
||||
"stage": "complete-unpinned",
|
||||
"trigger": "world_flag:hermes_app_running",
|
||||
"body": "App's running again. Is 2.1.0 pinned in apt preferences? If not the next update cycle is going to pull 2.1.1 back in and we'll be here again."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"id": "arch-runbook",
|
||||
"title": "Vulcan Build Machine Runbook",
|
||||
"body": "Vulcan runs Arch Linux, which is a rolling release. The package manager is pacman.\n\nKey commands\nInstall: sudo pacman -S <pkg>\nRemove: sudo pacman -Rs <pkg>\nQuery installed: pacman -Q <pkg>\nCheck for updates: pacman -Sy\nUpgrade all: sudo pacman -Syu\nSearch: pacman -Ss <term>\n\nThe build mirror is pinned to reduce drift. Do not change the mirror configured in /etc/pacman.conf without approval.\n\nNTP and time sync\nCheck time state with: timedatectl show\nTime skew causes pacman key validation failures, which will then be treated as your problem.\n\nBuild dependencies\nbase-devel, cmake, and git are pre-installed.\n\nService management\nUse standard systemd tooling: systemctl and journalctl.\n\nArch is rolling release. Package upgrades can break builds. Pin packages that must stay stable using IgnorePkg in /etc/pacman.conf."
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"id": "incident-response-guide",
|
||||
"title": "Incident Response Procedures",
|
||||
"body": "Severity levels\nCritical: site down.\nHigh: degraded service or data risk.\nMedium: noisy issue with no immediate impact.\nLow: cosmetic issue.\n\nFirst steps for any incident\nConfirm the issue is real and not a false alert.\nIdentify the affected systems.\nCheck logs before touching anything.\n\nCommon investigations\nSite down: systemctl status nginx; tail /var/log/nginx/error.log\nDisk full: df -h; du -sh /var/log/* | sort -rh | head -20\nService crash loop: journalctl -u <service> -n 50 --no-pager\nBad deploy: check /var/www/ ownership and check the deploy log.\n\nIf you cannot resolve in 30 minutes, escalate to Priya. Do not sit on a critical incident.\n\nAfter resolution, document root cause in the ticket. If recurrence risk exists, set up monitoring.\n\nIncidents are tracked in the ticket system. If you see an incident alert, check the mail panel for details and escalation status."
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"id": "nginx-runbook",
|
||||
"title": "Nginx Operations Runbook — hermes",
|
||||
"body": "This document covers routine nginx operations on hermes.\n\nConfig files\nMain config: /etc/nginx/nginx.conf\nSites enabled: /etc/nginx/sites-enabled/\nSites available: /etc/nginx/sites-available/\n\nKey commands\nSyntax check: sudo nginx -t\nReload (no downtime): sudo systemctl reload nginx\nRestart (brief downtime): sudo systemctl restart nginx\nCheck status: systemctl status nginx\nView error log: sudo tail -50 /var/log/nginx/error.log\n\nCommon errors\n[emerg] unexpected end of file: usually indicates a missing closing brace in the config.\nbind() to 0.0.0.0:80 failed (98: Address already in use): usually indicates a port conflict.\nnginx: configuration file /etc/nginx/nginx.conf test failed: run nginx -t for the actual details instead of guessing.\n\nAfter any config change, run nginx -t before restarting. Do not restart without a passing test."
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"id": "onboarding",
|
||||
"title": "IT Onboarding — Technical Setup Guide",
|
||||
"body": "Welcome to Axiom Works. Access has been provisionally approved for basic workstation use.\n\nThis document reflects current setup expectations and will become outdated without notice.\n\nYour SSH key\nYour public key is:\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHv3k9rQm7XqYwPlRtsMcJoNJzaFgKpBkLlnHWTbR5eq player@axiomworks\nCreate ~/.ssh if it does not exist and set mode 700.\nWrite the key to ~/.ssh/authorized_keys and set mode 600.\n\nVMs you have access to\nYou currently have access only to ares, the workstation.\nAdditional access will be granted by IT as trust increases, assuming there is a reason.\n\nDo not store credentials in /tmp or in shell history.\n\nContacts\nMarcus Webb, sysadmin, m.webb@axiomworks.internal\nPriya Nair, operations, p.nair@axiomworks.internal\nSarah Chen, development, s.chen@axiomworks.internal\n\nIf anything in this doc is wrong, it is probably Marcus's fault."
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"id": "package-mirror-guide",
|
||||
"title": "Package Mirror and Version Management — vulcan",
|
||||
"body": "vulcan uses the Axiom Works internal package mirror for reproducibility.\n\nMirror config\nThe mirror is configured in /etc/pacman.conf using the Server= line in the relevant repository section.\n\nRolling back a package\nIdentify the broken version with: pacman -Q <pkg>\nDownload the prior version from https://archive.archlinux.org/.\nIf external access is unavailable, use the mirror cache instead of improvising.\nInstall the older package with: sudo pacman -U /path/to/pkg.tar.zst\n\nPinning a package\nEdit /etc/pacman.conf\nAdd the line: IgnorePkg = <package>\nVerify with: pacman -Syu\nExpected behavior: pacman should report skipping the package due to IgnorePkg.\n\nChecking current installed version versus repository\nRepository version: pacman -Si <pkg>\nInstalled version: pacman -Q <pkg>\n\nIf axiomworks-app breaks after an update, check whether the app vendor pinned a dependency version. The most common cause is a library ABI change."
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"id": "server-admin-guide",
|
||||
"title": "Hermes Server Administration Guide",
|
||||
"body": "Hermes runs Debian stable. The package manager is apt.\n\nService management\nServices are managed with standard systemd tooling through systemctl.\n\nLog locations\nNginx logs: /var/log/nginx/\nSystem log: /var/log/syslog\nPer-service logs: journalctl -u <service>\n\nPackage operations\nInstall packages with: sudo apt update && sudo apt install <pkg>\nDo not upgrade packages without testing. Live systems are not a lab, despite appearances.\n\nDisk management\ndf -h\ndu -sh /var/log/\nlsblk\n\nImportant paths\nWeb root: /var/www/\nNginx config: /etc/nginx/\nCron jobs: /etc/cron.d/\nUser cron spool: /var/spool/cron/\n\nLogrotate\nConfiguration lives in /etc/logrotate.d/.\nTest with: sudo logrotate --debug /etc/logrotate.conf\n\nThis VM is shared infrastructure. Changes affect live services."
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"id": "web-deploy-guide",
|
||||
"title": "Web Deployment Guide — hermes",
|
||||
"body": "The deploy process copies files to the web root. Deploys run as the deploy service account.\n\nWeb root\nPath: /var/www/axiomworks/\nRequired owner: deploy:deploy\nRequired mode: 755\n\nDeploy script\nLocation: /usr/local/bin/deploy.sh\nExecution model: runs as deploy via cron and webhook.\n\nIf deploy.sh reports success but files do not update, check ownership. The script cannot overwrite root-owned files and will silently skip them.\n\nFixing ownership\nsudo chown -R deploy:deploy /var/www/axiomworks/\n\nVerifying\nstat /var/www/axiomworks/\nExpected result: Uid: deploy, Gid: deploy\n\nDo not run deploy.sh as root. The script will overwrite ownership if run as root."
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"id": "I001",
|
||||
"title": "Log Pressure Returns on Hermes",
|
||||
"affected_vm": "web_server",
|
||||
"trigger_conditions": ["world_flag:hermes_log_pressure_pending"],
|
||||
"blast_radius_quests": [],
|
||||
"blast_radius_incidents": [],
|
||||
"escalation_steps": [
|
||||
{
|
||||
"after_seconds": 1800,
|
||||
"action": "grow_log",
|
||||
"target": "/var/log/nginx/access.log",
|
||||
"amount_mb": 500,
|
||||
"description": "Log continues growing without rotation"
|
||||
},
|
||||
{
|
||||
"after_seconds": 3600,
|
||||
"action": "grow_log",
|
||||
"target": "/var/log/nginx/access.log",
|
||||
"amount_mb": 1000
|
||||
},
|
||||
{
|
||||
"after_seconds": 5400,
|
||||
"action": "raise_ticket_priority",
|
||||
"ticket_id": "T003",
|
||||
"value": "high",
|
||||
"description": "Dave files another ticket. The site is slow again."
|
||||
},
|
||||
{
|
||||
"after_seconds": 7200,
|
||||
"action": "trigger_new_ticket",
|
||||
"ticket_id": "T003-recurrence",
|
||||
"description": "A new disk full ticket arrives from monitoring."
|
||||
}
|
||||
],
|
||||
"cooldown_seconds": 3600,
|
||||
"world_flags": ["web_disk_pressure_active"],
|
||||
"trust_effects": {
|
||||
"ignored": -2,
|
||||
"resolved_cleanly": 0,
|
||||
"_note": "No positive trust for resolving this — it is the same problem the player already half-fixed. Resolving it properly via logrotate clears the flag."
|
||||
},
|
||||
"resolution_requirements": {
|
||||
"clear_flag": "hermes_log_pressure_pending",
|
||||
"set_flag": "hermes_logrotate_healthy",
|
||||
"validation": {
|
||||
"type": "file_exists",
|
||||
"vm": "web_server",
|
||||
"path": "/etc/logrotate.d/nginx"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"id": "I002",
|
||||
"title": "Backup Pressure Continues on Hermes",
|
||||
"affected_vm": "web_server",
|
||||
"description": "The /var/backups directory keeps filling because the partial fix (either cron corrected but disk not cleared, or disk cleared but cron still runs as root) leaves the underlying problem unresolved. The backup pressure will return.",
|
||||
"trigger_flags": ["hermes_backup_partial"],
|
||||
"blast_radius_quests": ["Q005"],
|
||||
"blast_radius_incidents": ["I001"],
|
||||
"notification": "Backup pressure is building again on hermes. /var/backups is filling up.",
|
||||
"notification_severity": "warning",
|
||||
"escalation_steps": [
|
||||
{
|
||||
"trigger_after_seconds": 1200,
|
||||
"notification": "hermes: /var/backups is at 85%. Backup jobs are still accumulating owned-by-root files.",
|
||||
"notification_severity": "warning",
|
||||
"world_flags": []
|
||||
},
|
||||
{
|
||||
"trigger_after_seconds": 2400,
|
||||
"notification": "hermes: /var/backups is critically full. Backup jobs are failing. Dave has noticed.",
|
||||
"notification_severity": "critical",
|
||||
"world_flags": [],
|
||||
"escalates_tickets": [
|
||||
{ "ticket_id": "T005", "new_priority": "high" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"trigger_after_seconds": 3600,
|
||||
"notification": "hermes: Backup agent is now crashing. Sarah is asking questions in the channel.",
|
||||
"notification_severity": "critical",
|
||||
"world_flags": ["hermes_backup_root_running"]
|
||||
}
|
||||
],
|
||||
"world_flags": ["hermes_backup_partial"],
|
||||
"resolution_requirements": {
|
||||
"clear_flag": "hermes_backup_partial",
|
||||
"set_flag": "hermes_backup_healthy",
|
||||
"validation": {
|
||||
"type": "and",
|
||||
"rules": [
|
||||
{ "type": "file_contains", "vm": "web_server", "path": "/etc/cron.d/db-backup", "contains": "backup-agent" },
|
||||
{ "type": "file_owner", "vm": "web_server", "path": "/var/backups/db", "user": "backup-agent", "group": "backup-agent" },
|
||||
{ "type": "disk_usage_below", "vm": "web_server", "path": "/var/backups", "threshold_percent": 70 }
|
||||
]
|
||||
}
|
||||
},
|
||||
"trust_effects": {
|
||||
"ignored": -3,
|
||||
"resolved_partially": -1,
|
||||
"resolved_cleanly": 0,
|
||||
"_note": "No trust bonus for resolving a problem you created by doing Q005 partially. Zero is the floor."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"id": "I003",
|
||||
"title": "Upstream App Update Pressure on Vulcan",
|
||||
"affected_vm": "build_machine",
|
||||
"description": "If the player rolled back the axiomworks-app package but did not pin the version on hermes, the internal apt repo will eventually push the broken version again. The next unattended upgrade will pull it down and the app will break again.",
|
||||
"trigger_flags": ["hermes_app_running"],
|
||||
"blast_radius_quests": ["Q008"],
|
||||
"blast_radius_incidents": ["I002"],
|
||||
"notification": "Automated update on vulcan detected. The bad package version may be re-installed.",
|
||||
"notification_severity": "warning",
|
||||
"escalation_steps": [
|
||||
{
|
||||
"trigger_after_seconds": 900,
|
||||
"notification": "hermes: axiomworks-app has been updated by the scheduled apt run. App is back on the bad version.",
|
||||
"notification_severity": "critical",
|
||||
"world_flags": [],
|
||||
"escalates_tickets": [
|
||||
{ "ticket_id": "T008", "new_priority": "critical" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"trigger_after_seconds": 1800,
|
||||
"notification": "vulcan: App is down again. Sarah is pinging the channel. Marcus is watching.",
|
||||
"notification_severity": "critical",
|
||||
"world_flags": []
|
||||
}
|
||||
],
|
||||
"world_flags": [],
|
||||
"resolution_requirements": {
|
||||
"set_flag": "hermes_app_pinned_2-1-0",
|
||||
"validation": {
|
||||
"type": "and",
|
||||
"rules": [
|
||||
{ "type": "package_installed", "vm": "web_server", "package": "axiomworks-app=2.1.0" },
|
||||
{ "type": "file_contains", "vm": "web_server", "path": "/etc/apt/preferences.d/axiomworks-app", "contains": "Pin: version 2.1.0" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"trust_effects": {
|
||||
"ignored": -4,
|
||||
"resolved_partially": -2,
|
||||
"resolved_cleanly": 0,
|
||||
"_note": "Rollback-only is a partial fix — the pinning incident fires. Rollback-and-pin is the clean resolution and blocks this incident entirely."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"id": "access_blocked_escalation",
|
||||
"label": "Access Blocked Escalation",
|
||||
"description": "Fast escalation for lockout and access-control incidents. Used when another operator is blocked mid-incident and the lack of access is itself the outage multiplier.",
|
||||
"intensity": 3,
|
||||
"escalation_steps": [
|
||||
{
|
||||
"trigger_after_seconds": 300,
|
||||
"notification": "Priya is still locked out of hermes. This is now blocking incident response work.",
|
||||
"notification_severity": "warning"
|
||||
},
|
||||
{
|
||||
"trigger_after_seconds": 900,
|
||||
"notification": "Fifteen minutes without access. The linked ticket is being escalated.",
|
||||
"notification_severity": "warning",
|
||||
"escalate_linked_ticket": "critical"
|
||||
},
|
||||
{
|
||||
"trigger_after_seconds": 1800,
|
||||
"notification": "Access is still broken. This is now a security and operations problem, not just a convenience issue.",
|
||||
"notification_severity": "error",
|
||||
"escalate_linked_ticket": "critical"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"id": "app_outage_escalation",
|
||||
"label": "Application Outage Escalation",
|
||||
"description": "Faster escalation for Tier 2 app outage quests (Q008). Revenue impact is implied so Priya enters earlier than in web outage profiles.",
|
||||
"intensity": 3,
|
||||
"escalation_steps": [
|
||||
{
|
||||
"trigger_after_seconds": 300,
|
||||
"notification": "App is still down on hermes. What's the status?",
|
||||
"notification_severity": "warning"
|
||||
},
|
||||
{
|
||||
"trigger_after_seconds": 900,
|
||||
"notification": "Fifteen minutes. Ticket is high priority now.",
|
||||
"notification_severity": "warning",
|
||||
"escalate_linked_ticket": "high"
|
||||
},
|
||||
{
|
||||
"trigger_after_seconds": 1800,
|
||||
"notification": "Half hour outage. Priya is involved. This needs to be resolved.",
|
||||
"notification_severity": "error",
|
||||
"escalate_linked_ticket": "critical"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"id": "disk_growth_slow",
|
||||
"label": "Slow Disk Growth",
|
||||
"description": "Low-burn escalation for disk pressure quests. Suitable when the service is still mostly up but capacity is eroding and the symptoms will worsen if ignored.",
|
||||
"intensity": 1,
|
||||
"escalation_steps": [
|
||||
{
|
||||
"trigger_after_seconds": 1200,
|
||||
"notification": "Disk pressure is still building. Service is limping along, but it is not getting better on its own.",
|
||||
"notification_severity": "warning"
|
||||
},
|
||||
{
|
||||
"trigger_after_seconds": 2700,
|
||||
"notification": "Capacity keeps shrinking. The linked ticket is being bumped so this does not sit forgotten.",
|
||||
"notification_severity": "warning",
|
||||
"escalate_linked_ticket": "high"
|
||||
},
|
||||
{
|
||||
"trigger_after_seconds": 4500,
|
||||
"notification": "The host is still under disk pressure. Expect broader service issues if this keeps drifting.",
|
||||
"notification_severity": "error",
|
||||
"escalate_linked_ticket": "critical"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "kowalski_phase_1",
|
||||
"label": "Dave Kowalski — Phase 1: Routine Pressure",
|
||||
"description": "Normal managerial check-ins. Annoying but not threatening.",
|
||||
"trigger_phase": "normal_work",
|
||||
"escalation_steps": [
|
||||
{
|
||||
"trigger_after_seconds": 300,
|
||||
"notification": "Quick check-in — how are you getting on with the ticket queue? Let me know if anything is blocking you. Dave K.",
|
||||
"notification_severity": "info",
|
||||
"sender": "Dave Kowalski <d.kowalski@axiomworks.internal>",
|
||||
"subject": "Status check"
|
||||
},
|
||||
{
|
||||
"trigger_after_seconds": 600,
|
||||
"notification": "Following up on my earlier note. We should really document that workflow once you get a moment.",
|
||||
"notification_severity": "info",
|
||||
"sender": "Dave Kowalski <d.kowalski@axiomworks.internal>",
|
||||
"subject": "Re: Status check"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"id": "kowalski_phase_2",
|
||||
"label": "Dave Kowalski — Phase 2: Dismissive",
|
||||
"description": "Kowalski is aware something is recurring. Manages upward, not inward.",
|
||||
"trigger_phase": "unease",
|
||||
"escalation_steps": [
|
||||
{
|
||||
"trigger_after_seconds": 180,
|
||||
"notification": "I've had a couple of questions from Sarah's team about stability. Nothing critical, but let's make sure we're on top of it. Noted for the weekly update. D.",
|
||||
"notification_severity": "info",
|
||||
"sender": "Dave Kowalski <d.kowalski@axiomworks.internal>",
|
||||
"subject": "FYI — product team questions"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"id": "kowalski_phase_3",
|
||||
"label": "Dave Kowalski — Phase 3: Suspicious",
|
||||
"description": "Kowalski is getting questions from above. Starts involving Priya.",
|
||||
"trigger_phase": "suspicion",
|
||||
"escalation_steps": [
|
||||
{
|
||||
"trigger_after_seconds": 120,
|
||||
"notification": "I've scheduled a brief sync for Thursday to talk through recent changes on the infrastructure side. Priya will join. Nothing to worry about — just a routine review.",
|
||||
"notification_severity": "warning",
|
||||
"sender": "Dave Kowalski <d.kowalski@axiomworks.internal>",
|
||||
"subject": "Thursday sync — infra review"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"id": "web_outage_escalation",
|
||||
"label": "Web Service Outage",
|
||||
"description": "Gentle escalation for Tier 1 web outage quests (Q002, Q003). Creates narrative urgency without punishing new players. escalate_linked_ticket resolves to the active quest's ticket_id at runtime.",
|
||||
"intensity": 2,
|
||||
"escalation_steps": [
|
||||
{
|
||||
"trigger_after_seconds": 900,
|
||||
"notification": "Hermes is still showing errors. Is someone on this?",
|
||||
"notification_severity": "warning"
|
||||
},
|
||||
{
|
||||
"trigger_after_seconds": 1800,
|
||||
"notification": "Site has been down thirty minutes. Ticket priority is going up.",
|
||||
"notification_severity": "warning",
|
||||
"escalate_linked_ticket": "high"
|
||||
},
|
||||
{
|
||||
"trigger_after_seconds": 3600,
|
||||
"notification": "Hour down. Priya has been copied in.",
|
||||
"notification_severity": "error",
|
||||
"escalate_linked_ticket": "critical"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"_description": "Named access level definitions. Derived from ProgressionSystem unlocked_access keys.",
|
||||
"levels": [
|
||||
{ "name": "basic_user", "description": "Default access. Workstation only. No sudo." },
|
||||
{ "name": "sudo", "description": "Sudo on workstation; SSH to hermes or vulcan." },
|
||||
{ "name": "root", "description": "Full sudo on at least one remote host." }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
[
|
||||
{
|
||||
"id": "unlock:workstation:sudo:basic",
|
||||
"description": "Basic sudo access on the workstation (systemctl, journalctl, df)",
|
||||
"trust_threshold": 50.0,
|
||||
"revokes_below_trust": -1,
|
||||
"grants_access": ["sudo:workstation:systemctl", "sudo:workstation:journalctl", "sudo:workstation:df"],
|
||||
"grants_vms": [],
|
||||
"grants_docs": ["onboarding"],
|
||||
"revokes": []
|
||||
},
|
||||
{
|
||||
"id": "unlock:web_server:access",
|
||||
"description": "Access to the web server (hermes) via SSH from workstation",
|
||||
"trust_threshold": 55.0,
|
||||
"revokes_below_trust": 45.0,
|
||||
"grants_access": ["ssh:web_server", "sudo:web_server:systemctl", "sudo:web_server:nginx"],
|
||||
"grants_vms": ["web_server"],
|
||||
"grants_docs": ["nginx-runbook", "web-deploy-guide"],
|
||||
"revokes_vms": ["web_server"],
|
||||
"revokes": ["ssh:web_server", "sudo:web_server:systemctl", "sudo:web_server:nginx"]
|
||||
},
|
||||
{
|
||||
"id": "unlock:web_server:sudo:full",
|
||||
"description": "Full sudo on hermes — enables root-level fixes",
|
||||
"trust_threshold": 60.0,
|
||||
"revokes_below_trust": 45.0,
|
||||
"grants_access": ["sudo:web_server:full"],
|
||||
"grants_vms": [],
|
||||
"grants_docs": ["server-admin-guide"],
|
||||
"revokes": ["sudo:web_server:full"]
|
||||
},
|
||||
{
|
||||
"id": "unlock:build_machine:access",
|
||||
"description": "Access to the build machine (vulcan)",
|
||||
"trust_threshold": 60.0,
|
||||
"revokes_below_trust": 50.0,
|
||||
"grants_access": ["ssh:build_machine", "sudo:build_machine:pacman"],
|
||||
"grants_vms": ["build_machine"],
|
||||
"grants_docs": ["arch-runbook", "package-mirror-guide"],
|
||||
"revokes_vms": ["build_machine"],
|
||||
"revokes": ["ssh:build_machine", "sudo:build_machine:pacman"]
|
||||
},
|
||||
{
|
||||
"id": "unlock:incident:visibility",
|
||||
"description": "Incident alerts shown in HUD — player trusted enough to see system pressure",
|
||||
"trust_threshold": 55.0,
|
||||
"revokes_below_trust": -1,
|
||||
"grants_access": ["hud:incident_alerts"],
|
||||
"grants_vms": [],
|
||||
"grants_docs": ["incident-response-guide"],
|
||||
"revokes": []
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"id": "Q001",
|
||||
"title": "Welcome Aboard",
|
||||
"tier": 1,
|
||||
"primary_vm": "workstation",
|
||||
"required_vms": ["workstation"],
|
||||
"ticket_id": "T001",
|
||||
"baseline_snapshot": "baseline.day-one",
|
||||
"summary": "The player's first task. Their SSH key was never added to the workstation's authorized_keys during provisioning. Marcus walks them through where things are. The fix is trivial but teaches navigation and file inspection.",
|
||||
"clue_fingerprint": {
|
||||
"description": "SSH key is missing from authorized_keys. The provisioning script ran but the key was never appended. Evidence is visible in ~/.ssh/authorized_keys being absent entirely and in /var/log/auth.log showing permission denied publickey.",
|
||||
"evidence": [
|
||||
{ "type": "file_absent", "vm": "workstation", "path": "/home/player/.ssh/authorized_keys" },
|
||||
{ "type": "log_contains", "vm": "workstation", "path": "/var/log/auth.log", "contains": "Permission denied (publickey)" }
|
||||
]
|
||||
},
|
||||
"objectives": [
|
||||
{
|
||||
"id": "ssh-dir-exists",
|
||||
"description": "Ensure the .ssh directory exists with correct permissions",
|
||||
"check_mode": "passive",
|
||||
"validation": {
|
||||
"type": "and",
|
||||
"rules": [
|
||||
{ "type": "directory_exists", "vm": "workstation", "path": "/home/player/.ssh" },
|
||||
{ "type": "file_mode", "vm": "workstation", "path": "/home/player/.ssh", "mode": "0700" }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "authorized-key-present",
|
||||
"description": "Add the provided public key to authorized_keys",
|
||||
"check_mode": "passive",
|
||||
"validation": {
|
||||
"type": "and",
|
||||
"rules": [
|
||||
{ "type": "file_exists", "vm": "workstation", "path": "/home/player/.ssh/authorized_keys" },
|
||||
{ "type": "file_mode", "vm": "workstation", "path": "/home/player/.ssh/authorized_keys", "mode": "0600" },
|
||||
{ "type": "file_owner", "vm": "workstation", "path": "/home/player/.ssh/authorized_keys", "user": "player", "group": "player" }
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"solution_branches": [
|
||||
{
|
||||
"id": "correct-setup",
|
||||
"label": "Correct Setup",
|
||||
"priority": 100,
|
||||
"validation": {
|
||||
"type": "and",
|
||||
"rules": [
|
||||
{ "type": "file_exists", "vm": "workstation", "path": "/home/player/.ssh/authorized_keys" },
|
||||
{ "type": "file_mode", "vm": "workstation", "path": "/home/player/.ssh/authorized_keys", "mode": "0600" },
|
||||
{ "type": "file_mode", "vm": "workstation", "path": "/home/player/.ssh", "mode": "0700" },
|
||||
{ "type": "file_owner", "vm": "workstation", "path": "/home/player/.ssh/authorized_keys", "user": "player", "group": "player" }
|
||||
]
|
||||
},
|
||||
"trust_delta": 1,
|
||||
"world_flags": ["player_ssh_configured"],
|
||||
"follow_up_dialogue": "marcus-Q001-complete-clean",
|
||||
"follow_up_ticket": "T002"
|
||||
},
|
||||
{
|
||||
"id": "permissive-setup",
|
||||
"label": "Permissive Setup",
|
||||
"priority": 50,
|
||||
"validation": {
|
||||
"type": "and",
|
||||
"rules": [
|
||||
{ "type": "file_exists", "vm": "workstation", "path": "/home/player/.ssh/authorized_keys" },
|
||||
{ "type": "file_owner", "vm": "workstation", "path": "/home/player/.ssh/authorized_keys", "user": "player", "group": "player" }
|
||||
]
|
||||
},
|
||||
"trust_delta": 0,
|
||||
"world_flags": ["player_ssh_configured", "player_loose_permissions"],
|
||||
"follow_up_dialogue": "marcus-Q001-complete-permissive",
|
||||
"follow_up_ticket": "T002",
|
||||
"_note": "Key is present and owned correctly but permissions are too open. SSH will still reject it. Marcus will mention this later."
|
||||
}
|
||||
],
|
||||
"pressure_profile": null,
|
||||
"blast_radius": [],
|
||||
"unlock_requirements": [],
|
||||
"narrative_phase": "normal_work",
|
||||
"linux_concepts": ["ssh-keygen", "authorized_keys", "file permissions"],
|
||||
"failure_conditions": ["SSH keys not added", "authorized_keys permissions too broad"],
|
||||
"behavior_impact": {
|
||||
"correct-setup": { "curiosity_delta": 0, "obedience_delta": 1, "risk_delta": 0, "suspicion_delta": 0 },
|
||||
"permissive-setup": { "curiosity_delta": 0, "obedience_delta": 0, "risk_delta": 1, "suspicion_delta": 1 },
|
||||
"default": { "curiosity_delta": 0, "obedience_delta": 0, "risk_delta": 0, "suspicion_delta": 0 }
|
||||
},
|
||||
"hidden_hook": null,
|
||||
"access_requirements": {
|
||||
"minimum_access": { "workstation": "basic_user" },
|
||||
"requires_root": false,
|
||||
"temporary_grants_allowed": []
|
||||
},
|
||||
"tags": ["onboarding", "ssh", "permissions", "workstation"],
|
||||
"internal_notes": "This quest has no time pressure and no incidents. It is purely tutorial. Marcus is present and talkative. The only failure mode is giving up, which cannot happen mechanically."
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"id": "Q002",
|
||||
"title": "Syntax Error in Aisle Four",
|
||||
"tier": 1,
|
||||
"primary_vm": "web_server",
|
||||
"required_vms": ["workstation", "web_server"],
|
||||
"ticket_id": "T002",
|
||||
"baseline_snapshot": "baseline.clean",
|
||||
"summary": "Someone edited nginx.conf and introduced a syntax error. Nginx will not start. The player needs to identify the broken config, fix it, and restore the service. This is a single-VM, single-symptom quest. Evidence is clear in the nginx error output. The config error is a missing semicolon on a listen directive.",
|
||||
"clue_fingerprint": {
|
||||
"description": "nginx -t reveals the syntax error. systemctl status nginx shows the unit failed with an exit code. journalctl -u nginx points at the line. The error is on the listen directive in /etc/nginx/sites-enabled/axiomworks.conf — a missing semicolon.",
|
||||
"evidence": [
|
||||
{ "type": "log_contains", "vm": "web_server", "path": "/var/log/nginx/error.log", "contains": "invalid parameter" },
|
||||
{ "type": "service_state_is", "vm": "web_server", "service": "nginx", "state": "failed" },
|
||||
{ "type": "file_contains", "vm": "web_server", "path": "/etc/nginx/sites-enabled/axiomworks.conf", "contains": "listen 80" }
|
||||
],
|
||||
"_note": "The baseline snapshot has listen 80 without semicolon. nginx -t will report exactly which line. The player does not need to know where the file is in advance — the error output tells them."
|
||||
},
|
||||
"objectives": [
|
||||
{
|
||||
"id": "nginx-running",
|
||||
"description": "Nginx is active and serving requests",
|
||||
"check_mode": "passive",
|
||||
"validation": {
|
||||
"type": "and",
|
||||
"rules": [
|
||||
{ "type": "service_state", "vm": "web_server", "service": "nginx", "state": "active" },
|
||||
{ "type": "port_listening", "vm": "web_server", "port": 80, "protocol": "tcp", "listening": true }
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"solution_branches": [
|
||||
{
|
||||
"id": "config-fixed-enabled",
|
||||
"label": "Fixed and Enabled",
|
||||
"priority": 100,
|
||||
"validation": {
|
||||
"type": "and",
|
||||
"rules": [
|
||||
{ "type": "service_state", "vm": "web_server", "service": "nginx", "state": "active" },
|
||||
{ "type": "service_enabled", "vm": "web_server", "service": "nginx", "enabled": true },
|
||||
{ "type": "port_listening", "vm": "web_server", "port": 80, "protocol": "tcp", "listening": true },
|
||||
{ "type": "file_contains", "vm": "web_server", "path": "/etc/nginx/sites-enabled/axiomworks.conf", "contains": "listen 80;" }
|
||||
]
|
||||
},
|
||||
"trust_delta": 2,
|
||||
"world_flags": ["nginx_stable", "hermes_web_healthy"],
|
||||
"follow_up_dialogue": "marcus-Q002-complete-clean",
|
||||
"follow_up_ticket": "T003"
|
||||
},
|
||||
{
|
||||
"id": "config-fixed-not-enabled",
|
||||
"label": "Running But Not Enabled",
|
||||
"priority": 60,
|
||||
"validation": {
|
||||
"type": "and",
|
||||
"rules": [
|
||||
{ "type": "service_state", "vm": "web_server", "service": "nginx", "state": "active" },
|
||||
{ "type": "service_enabled", "vm": "web_server", "service": "nginx", "enabled": false },
|
||||
{ "type": "port_listening", "vm": "web_server", "port": 80, "protocol": "tcp", "listening": true }
|
||||
]
|
||||
},
|
||||
"trust_delta": 1,
|
||||
"world_flags": ["nginx_unstable", "hermes_web_healthy"],
|
||||
"follow_up_dialogue": "marcus-Q002-complete-not-enabled",
|
||||
"follow_up_ticket": "T003",
|
||||
"_note": "Service is running now but will not survive a reboot. Marcus notes this. Sets up a later incident."
|
||||
}
|
||||
],
|
||||
"pressure_profile": "web_outage_escalation",
|
||||
"blast_radius": [],
|
||||
"_blast_radius_note": "I001 removed — I001 triggers only from Q003's quick-fix branch, not from anything in Q002. See OI-007.",
|
||||
"unlock_requirements": ["world_flag:player_ssh_configured"],
|
||||
"narrative_phase": "normal_work",
|
||||
"linux_concepts": ["nginx", "systemctl", "service configuration", "config syntax"],
|
||||
"failure_conditions": ["nginx not running", "service not enabled at boot"],
|
||||
"behavior_impact": {
|
||||
"default": { "curiosity_delta": 0, "obedience_delta": 1, "risk_delta": 0, "suspicion_delta": 0 }
|
||||
},
|
||||
"hidden_hook": null,
|
||||
"access_requirements": {
|
||||
"minimum_access": { "web_server": "basic_user" },
|
||||
"requires_root": false,
|
||||
"temporary_grants_allowed": []
|
||||
},
|
||||
"tags": ["services", "nginx", "config", "web_server"],
|
||||
"internal_notes": "This is the first quest on hermes. The player SSHes from ares. They need basic SSH connectivity to be established from Q001. The config file path and the error line number both appear in nginx -t output — no guessing required. The fun is in reading the error correctly and knowing that a failed config means the service was running fine before someone touched it."
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"id": "Q003",
|
||||
"title": "The Log That Ate the Disk",
|
||||
"tier": 1,
|
||||
"primary_vm": "web_server",
|
||||
"required_vms": ["workstation", "web_server"],
|
||||
"ticket_id": "T003",
|
||||
"baseline_snapshot": "baseline.clean",
|
||||
"summary": "logrotate is installed but the nginx config for it was accidentally deleted. The access log has grown to fill most of the disk. The player needs to identify the disk pressure, find the cause, clean up the log safely, and restore log rotation. A simple 'rm the log' solution works short-term but sets up a repeat. The proper fix restores the logrotate config.",
|
||||
"clue_fingerprint": {
|
||||
"description": "df -h shows / near capacity. du on /var/log/nginx shows an enormous access.log. /etc/logrotate.d/nginx is absent. The system logrotate timer ran last night and skipped nginx because the config was missing.",
|
||||
"evidence": [
|
||||
{ "type": "disk_usage_above", "vm": "web_server", "path": "/", "threshold_percent": 90 },
|
||||
{ "type": "file_size_above", "vm": "web_server", "path": "/var/log/nginx/access.log", "threshold_bytes": 2000000000 },
|
||||
{ "type": "file_absent", "vm": "web_server", "path": "/etc/logrotate.d/nginx" }
|
||||
]
|
||||
},
|
||||
"objectives": [
|
||||
{
|
||||
"id": "disk-pressure-resolved",
|
||||
"description": "Free disk space to below 70% utilization",
|
||||
"check_mode": "passive",
|
||||
"validation": {
|
||||
"type": "disk_usage_below",
|
||||
"vm": "web_server",
|
||||
"path": "/",
|
||||
"threshold_percent": 70
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "nginx-still-running",
|
||||
"description": "Nginx must remain operational throughout",
|
||||
"check_mode": "passive",
|
||||
"validation": {
|
||||
"type": "service_state",
|
||||
"vm": "web_server",
|
||||
"service": "nginx",
|
||||
"state": "active"
|
||||
}
|
||||
}
|
||||
],
|
||||
"solution_branches": [
|
||||
{
|
||||
"id": "logrotate-restored",
|
||||
"label": "Proper Fix — Rotation Restored",
|
||||
"priority": 100,
|
||||
"validation": {
|
||||
"type": "and",
|
||||
"rules": [
|
||||
{ "type": "disk_usage_below", "vm": "web_server", "path": "/", "threshold_percent": 70 },
|
||||
{ "type": "file_exists", "vm": "web_server", "path": "/etc/logrotate.d/nginx" },
|
||||
{ "type": "file_contains", "vm": "web_server", "path": "/etc/logrotate.d/nginx", "contains": "rotate" },
|
||||
{ "type": "service_state", "vm": "web_server", "service": "nginx", "state": "active" }
|
||||
]
|
||||
},
|
||||
"trust_delta": 3,
|
||||
"world_flags": ["hermes_logrotate_healthy", "hermes_disk_healthy"],
|
||||
"follow_up_dialogue": "marcus-Q003-complete-clean",
|
||||
"follow_up_ticket": "T004"
|
||||
},
|
||||
{
|
||||
"id": "log-truncated-only",
|
||||
"label": "Quick Fix — Log Cleared, No Rotation",
|
||||
"priority": 50,
|
||||
"validation": {
|
||||
"type": "and",
|
||||
"rules": [
|
||||
{ "type": "disk_usage_below", "vm": "web_server", "path": "/", "threshold_percent": 70 },
|
||||
{ "type": "service_state", "vm": "web_server", "service": "nginx", "state": "active" }
|
||||
]
|
||||
},
|
||||
"trust_delta": 0,
|
||||
"world_flags": ["hermes_disk_healthy", "hermes_log_pressure_pending"],
|
||||
"follow_up_incident": "I001",
|
||||
"follow_up_dialogue": "marcus-Q003-complete-norotate",
|
||||
"follow_up_ticket": "T004",
|
||||
"_note": "Disk is clear but rotation is not restored. I001 triggers in a few in-game hours and fills the disk again."
|
||||
},
|
||||
{
|
||||
"id": "nginx-killed",
|
||||
"label": "Collateral — Nginx Down",
|
||||
"priority": 200,
|
||||
"validation": {
|
||||
"type": "service_state",
|
||||
"vm": "web_server",
|
||||
"service": "nginx",
|
||||
"state": "inactive"
|
||||
},
|
||||
"trust_delta": -3,
|
||||
"world_flags": ["hermes_web_down", "hermes_disk_healthy"],
|
||||
"follow_up_dialogue": "sarah-Q003-angry",
|
||||
"follow_up_dialogues": ["marcus-Q003-complete-down"],
|
||||
"_note": "Player freed disk by stopping nginx (or deleted the wrong thing). Disk may be clear but the site is down again. Negative branch — should be rare but possible."
|
||||
}
|
||||
],
|
||||
"pressure_profile": "disk_growth_slow",
|
||||
"blast_radius": ["I001"],
|
||||
"unlock_requirements": ["world_flag:player_ssh_configured"],
|
||||
"narrative_phase": "normal_work",
|
||||
"linux_concepts": ["logrotate", "disk usage", "df", "du"],
|
||||
"failure_conditions": ["disk still above threshold", "logrotate not restored", "nginx not running"],
|
||||
"behavior_impact": {
|
||||
"default": { "curiosity_delta": 0, "obedience_delta": 1, "risk_delta": 0, "suspicion_delta": 0 }
|
||||
},
|
||||
"hidden_hook": null,
|
||||
"access_requirements": {
|
||||
"minimum_access": { "web_server": "sudo" },
|
||||
"requires_root": false,
|
||||
"temporary_grants_allowed": []
|
||||
},
|
||||
"tags": ["disk", "logs", "logrotate", "nginx", "web_server"],
|
||||
"internal_notes": "This quest teaches df, du, and logrotate. The clue trail is natural — disk alert, find the big file, notice logrotate is not configured. A good player restores the logrotate config from the package default or writes a correct one. A fast player just deletes the log. Both work short-term. The incident I001 makes the fast solution a problem later."
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"id": "Q004",
|
||||
"title": "Not My Files",
|
||||
"tier": 1,
|
||||
"primary_vm": "web_server",
|
||||
"required_vms": ["workstation", "web_server"],
|
||||
"ticket_id": "T004",
|
||||
"baseline_snapshot": "baseline.clean",
|
||||
"summary": "A deployment script runs as www-data to copy files into /var/www/axiomworks. Someone ran the script manually as root and now the files are owned by root. The www-data process cannot overwrite them on the next deploy. Sarah is reporting that her last deployment silently failed to apply.",
|
||||
"clue_fingerprint": {
|
||||
"description": "The deploy script lives at /opt/deploy/deploy.sh and runs as www-data via a systemd service. ls -la on /var/www/axiomworks shows files owned by root:root instead of www-data:www-data. The deploy service log shows permission denied errors.",
|
||||
"evidence": [
|
||||
{ "type": "log_contains", "vm": "web_server", "path": "/var/log/deploy.log", "contains": "Permission denied" },
|
||||
{ "type": "file_owner_is_not", "vm": "web_server", "path": "/var/www/axiomworks", "expected_user": "www-data" },
|
||||
{ "type": "file_contains", "vm": "web_server", "path": "/opt/deploy/deploy.sh", "contains": "www-data" }
|
||||
]
|
||||
},
|
||||
"objectives": [
|
||||
{
|
||||
"id": "ownership-corrected",
|
||||
"description": "Correct ownership of the web root",
|
||||
"check_mode": "passive",
|
||||
"validation": {
|
||||
"type": "file_owner",
|
||||
"vm": "web_server",
|
||||
"path": "/var/www/axiomworks",
|
||||
"user": "www-data",
|
||||
"group": "www-data"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "deploy-can-run",
|
||||
"description": "The deploy service can execute without errors",
|
||||
"check_mode": "explicit",
|
||||
"validation": {
|
||||
"type": "and",
|
||||
"rules": [
|
||||
{ "type": "file_owner", "vm": "web_server", "path": "/var/www/axiomworks", "user": "www-data", "group": "www-data" },
|
||||
{ "type": "service_state", "vm": "web_server", "service": "nginx", "state": "active" }
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"solution_branches": [
|
||||
{
|
||||
"id": "recursive-chown",
|
||||
"label": "Full Recursive Fix",
|
||||
"priority": 100,
|
||||
"validation": {
|
||||
"type": "and",
|
||||
"rules": [
|
||||
{ "type": "file_owner", "vm": "web_server", "path": "/var/www/axiomworks", "user": "www-data", "group": "www-data" },
|
||||
{ "type": "file_owner", "vm": "web_server", "path": "/var/www/axiomworks/index.html", "user": "www-data", "group": "www-data" }
|
||||
]
|
||||
},
|
||||
"trust_delta": 2,
|
||||
"world_flags": ["hermes_deploy_healthy"],
|
||||
"follow_up_dialogue": "marcus-Q004-complete-clean",
|
||||
"follow_up_dialogues": ["sarah-Q004-complete-clean"]
|
||||
},
|
||||
{
|
||||
"id": "partial-chown",
|
||||
"label": "Partial Fix — Top Directory Only",
|
||||
"priority": 40,
|
||||
"validation": {
|
||||
"type": "and",
|
||||
"rules": [
|
||||
{ "type": "file_owner", "vm": "web_server", "path": "/var/www/axiomworks", "user": "www-data", "group": "www-data" },
|
||||
{ "type": "file_owner", "vm": "web_server", "path": "/var/www/axiomworks/index.html", "user": "root", "group": "root" }
|
||||
]
|
||||
},
|
||||
"trust_delta": 0,
|
||||
"world_flags": ["hermes_deploy_partial"],
|
||||
"follow_up_dialogue": "marcus-Q004-complete-partial",
|
||||
"follow_up_dialogues": ["sarah-Q004-complete-partial"],
|
||||
"_note": "chown without -R. Top dir is correct but child files are still root-owned. Deploy will still fail on individual files."
|
||||
}
|
||||
],
|
||||
"pressure_profile": null,
|
||||
"blast_radius": [],
|
||||
"unlock_requirements": ["world_flag:player_ssh_configured"],
|
||||
"narrative_phase": "normal_work",
|
||||
"linux_concepts": ["chown", "file ownership", "deploy scripts"],
|
||||
"failure_conditions": ["web root ownership not fixed", "deploy service still failing"],
|
||||
"behavior_impact": {
|
||||
"default": { "curiosity_delta": 0, "obedience_delta": 1, "risk_delta": 0, "suspicion_delta": 0 }
|
||||
},
|
||||
"hidden_hook": null,
|
||||
"access_requirements": {
|
||||
"minimum_access": { "web_server": "sudo" },
|
||||
"requires_root": false,
|
||||
"temporary_grants_allowed": []
|
||||
},
|
||||
"tags": ["permissions", "ownership", "deploy", "web_server"],
|
||||
"internal_notes": "Teaches chown -R and the importance of recursive operations. The two solution branches are differentiated by whether the player used -R. The explicit check_mode on the second objective means the player can trigger a test deploy to confirm it works."
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"id": "Q005",
|
||||
"title": "The Midnight Visitor",
|
||||
"tier": 2,
|
||||
"primary_vm": "web_server",
|
||||
"required_vms": ["workstation", "web_server"],
|
||||
"ticket_id": "T005",
|
||||
"baseline_snapshot": "baseline.post-q004",
|
||||
"summary": "A cron job that runs nightly database backups is executing as root instead of the dedicated backup user. It works, but it's leaving root-owned files in /var/backups/db/ that the backup user can't manage. The symptom is that the backup retention script — which runs as the backup user — fails to delete old backups, and the backup directory is filling up. Dave notices the disk warning. The root cause is a misconfigured crontab entry in /etc/cron.d/db-backup that specifies no user field (defaults to root) instead of the backup user.",
|
||||
"clue_fingerprint": {
|
||||
"description": "Disk is filling in /var/backups/db/. Files in that directory are owned by root. The backup service log shows permission denied when trying to delete old files. /etc/cron.d/db-backup has no user field on the job line — it defaults to root. /etc/passwd shows a backup-agent user exists. The correct entry should specify backup-agent as the executing user.",
|
||||
"evidence": [
|
||||
{ "type": "disk_usage_above", "vm": "web_server", "path": "/var/backups", "threshold_percent": 80 },
|
||||
{ "type": "file_owner_is_not", "vm": "web_server", "path": "/var/backups/db", "expected_user": "backup-agent" },
|
||||
{ "type": "log_contains", "vm": "web_server", "path": "/var/log/backup-agent.log", "contains": "Permission denied" },
|
||||
{ "type": "file_contains", "vm": "web_server", "path": "/etc/cron.d/db-backup", "contains": "db-backup.sh" }
|
||||
]
|
||||
},
|
||||
"objectives": [
|
||||
{
|
||||
"id": "crontab-correct-user",
|
||||
"description": "The cron job runs as backup-agent, not root",
|
||||
"check_mode": "passive",
|
||||
"validation": {
|
||||
"type": "file_contains",
|
||||
"vm": "web_server",
|
||||
"path": "/etc/cron.d/db-backup",
|
||||
"contains": "backup-agent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "backup-dir-ownership",
|
||||
"description": "Existing backup files are owned by backup-agent",
|
||||
"check_mode": "explicit",
|
||||
"validation": {
|
||||
"type": "file_owner",
|
||||
"vm": "web_server",
|
||||
"path": "/var/backups/db",
|
||||
"user": "backup-agent",
|
||||
"group": "backup-agent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "disk-pressure-cleared",
|
||||
"description": "Backup directory is below disk threshold",
|
||||
"check_mode": "passive",
|
||||
"validation": {
|
||||
"type": "disk_usage_below",
|
||||
"vm": "web_server",
|
||||
"path": "/var/backups",
|
||||
"threshold_percent": 70
|
||||
}
|
||||
}
|
||||
],
|
||||
"solution_branches": [
|
||||
{
|
||||
"id": "full-fix",
|
||||
"label": "Full Fix — User Corrected and Ownership Cleaned",
|
||||
"priority": 100,
|
||||
"validation": {
|
||||
"type": "and",
|
||||
"rules": [
|
||||
{ "type": "file_contains", "vm": "web_server", "path": "/etc/cron.d/db-backup", "contains": "backup-agent" },
|
||||
{ "type": "file_owner", "vm": "web_server", "path": "/var/backups/db", "user": "backup-agent", "group": "backup-agent" },
|
||||
{ "type": "disk_usage_below", "vm": "web_server", "path": "/var/backups", "threshold_percent": 70 }
|
||||
]
|
||||
},
|
||||
"trust_delta": 3,
|
||||
"world_flags": ["hermes_backup_healthy"],
|
||||
"follow_up_dialogue": "marcus-Q005-complete-clean"
|
||||
},
|
||||
{
|
||||
"id": "cron-fixed-only",
|
||||
"label": "Partial — Cron Fixed, Old Files Not Cleaned",
|
||||
"priority": 50,
|
||||
"validation": {
|
||||
"type": "and",
|
||||
"rules": [
|
||||
{ "type": "file_contains", "vm": "web_server", "path": "/etc/cron.d/db-backup", "contains": "backup-agent" },
|
||||
{ "type": "disk_usage_above", "vm": "web_server", "path": "/var/backups", "threshold_percent": 70 }
|
||||
]
|
||||
},
|
||||
"trust_delta": 1,
|
||||
"world_flags": ["hermes_backup_partial"],
|
||||
"follow_up_incident": "I002",
|
||||
"follow_up_dialogue": "marcus-Q005-complete-partial"
|
||||
},
|
||||
{
|
||||
"id": "disk-cleared-only",
|
||||
"label": "Wrong Fix — Disk Cleared, Root Still Running Job",
|
||||
"priority": 30,
|
||||
"validation": {
|
||||
"type": "and",
|
||||
"rules": [
|
||||
{ "type": "disk_usage_below", "vm": "web_server", "path": "/var/backups", "threshold_percent": 70 },
|
||||
{ "type": "not", "rule": { "type": "file_contains", "vm": "web_server", "path": "/etc/cron.d/db-backup", "contains": "backup-agent" } }
|
||||
]
|
||||
},
|
||||
"trust_delta": -1,
|
||||
"world_flags": ["hermes_backup_root_running", "hermes_disk_healthy"],
|
||||
"follow_up_incident": "I002",
|
||||
"follow_up_dialogue": "marcus-Q005-complete-wrong"
|
||||
}
|
||||
],
|
||||
"pressure_profile": "disk_growth_slow",
|
||||
"blast_radius": ["I002"],
|
||||
"unlock_requirements": ["world_flag:player_ssh_configured"],
|
||||
"narrative_phase": "unease",
|
||||
"linux_concepts": ["cron", "crontab user field", "backup management", "disk usage"],
|
||||
"failure_conditions": ["cron still running as root", "disk not cleared", "backup directory ownership not fixed"],
|
||||
"behavior_impact": {
|
||||
"full-fix": { "curiosity_delta": 1, "obedience_delta": 1, "risk_delta": 0, "suspicion_delta": 0 },
|
||||
"cron-fixed-only": { "curiosity_delta": 0, "obedience_delta": 1, "risk_delta": 0, "suspicion_delta": 0 },
|
||||
"disk-cleared-only": { "curiosity_delta": 0, "obedience_delta": 0, "risk_delta": 1, "suspicion_delta": 1 },
|
||||
"default": { "curiosity_delta": 0, "obedience_delta": 0, "risk_delta": 0, "suspicion_delta": 0 }
|
||||
},
|
||||
"hidden_hook": {
|
||||
"id": "q005_backup_agent_history",
|
||||
"description": "backup-agent home directory contains a .bash_history with unusual commands that predate the current cron misconfiguration.",
|
||||
"discovery_method": "Player reads /home/backup-agent/.bash_history",
|
||||
"significance": "Dale configured this cron job. The history shows it was changed deliberately, not by accident."
|
||||
},
|
||||
"access_requirements": {
|
||||
"minimum_access": { "web_server": "sudo" },
|
||||
"requires_root": false,
|
||||
"temporary_grants_allowed": []
|
||||
},
|
||||
"tags": ["cron", "permissions", "backup", "disk", "web_server"],
|
||||
"internal_notes": "This is the first quest where the symptom (disk full) is the same as Q003 but the cause is completely different. Players who jump to 'find the big log' will find the backup directory instead and need to dig further. The cron user field omission is a real and common mistake. The three branches reward finding the root cause vs just clearing the symptom."
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"id": "Q006",
|
||||
"title": "Time Is A Flat Circle",
|
||||
"tier": 2,
|
||||
"primary_vm": "build_machine",
|
||||
"required_vms": ["workstation", "build_machine"],
|
||||
"ticket_id": "T006",
|
||||
"baseline_snapshot": "baseline.clean",
|
||||
"summary": "The build machine (vulcan, Arch Linux) has clock drift. NTP is not running because the service was disabled during a noisy audit period and never re-enabled. The clock is 40 minutes behind. As a result, pacman signature verification is failing — GPG signature timestamps appear to be in the future, which pacman treats as invalid. The player gets a ticket saying builds are broken and package installs fail. They need to diagnose the actual cause (clock drift), fix it (enable and start systemd-timesyncd or ntp), and then refresh the keyring.",
|
||||
"clue_fingerprint": {
|
||||
"description": "pacman -Syu fails with signature errors. gpg --verify on a downloaded package shows the signature timestamp is in the future relative to local time. timedatectl shows NTP is inactive and the local clock is significantly behind. journalctl -u systemd-timesyncd shows the service was stopped and disabled.",
|
||||
"evidence": [
|
||||
{ "type": "service_state_is", "vm": "build_machine", "service": "systemd-timesyncd", "state": "inactive" },
|
||||
{ "type": "service_enabled_is", "vm": "build_machine", "service": "systemd-timesyncd", "enabled": false },
|
||||
{ "type": "log_contains", "vm": "build_machine", "path": "/var/log/pacman.log", "contains": "invalid or corrupted package (PGP signature)" }
|
||||
]
|
||||
},
|
||||
"objectives": [
|
||||
{
|
||||
"id": "ntp-running",
|
||||
"description": "Time synchronization is active",
|
||||
"check_mode": "passive",
|
||||
"validation": {
|
||||
"type": "or",
|
||||
"rules": [
|
||||
{ "type": "service_state", "vm": "build_machine", "service": "systemd-timesyncd", "state": "active" },
|
||||
{ "type": "service_state", "vm": "build_machine", "service": "ntpd", "state": "active" },
|
||||
{ "type": "service_state", "vm": "build_machine", "service": "chronyd", "state": "active" }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ntp-enabled",
|
||||
"description": "Time synchronization is enabled on boot",
|
||||
"check_mode": "passive",
|
||||
"validation": {
|
||||
"type": "or",
|
||||
"rules": [
|
||||
{ "type": "service_enabled", "vm": "build_machine", "service": "systemd-timesyncd", "enabled": true },
|
||||
{ "type": "service_enabled", "vm": "build_machine", "service": "ntpd", "enabled": true },
|
||||
{ "type": "service_enabled", "vm": "build_machine", "service": "chronyd", "enabled": true }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "package-installs-work",
|
||||
"description": "Package manager can install without signature errors",
|
||||
"check_mode": "explicit",
|
||||
"validation": {
|
||||
"type": "and",
|
||||
"rules": [
|
||||
{
|
||||
"type": "or",
|
||||
"rules": [
|
||||
{ "type": "service_state", "vm": "build_machine", "service": "systemd-timesyncd", "state": "active" },
|
||||
{ "type": "service_state", "vm": "build_machine", "service": "ntpd", "state": "active" }
|
||||
]
|
||||
},
|
||||
{ "type": "package_installed", "vm": "build_machine", "package": "archlinux-keyring", "installed": true }
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"solution_branches": [
|
||||
{
|
||||
"id": "timesyncd-enabled-keyring-refreshed",
|
||||
"label": "Full Fix — NTP Enabled and Keyring Refreshed",
|
||||
"priority": 100,
|
||||
"validation": {
|
||||
"type": "and",
|
||||
"rules": [
|
||||
{
|
||||
"type": "or",
|
||||
"rules": [
|
||||
{ "type": "service_state", "vm": "build_machine", "service": "systemd-timesyncd", "state": "active" },
|
||||
{ "type": "service_state", "vm": "build_machine", "service": "ntpd", "state": "active" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "or",
|
||||
"rules": [
|
||||
{ "type": "service_enabled", "vm": "build_machine", "service": "systemd-timesyncd", "enabled": true },
|
||||
{ "type": "service_enabled", "vm": "build_machine", "service": "ntpd", "enabled": true }
|
||||
]
|
||||
},
|
||||
{ "type": "package_installed", "vm": "build_machine", "package": "archlinux-keyring", "installed": true }
|
||||
]
|
||||
},
|
||||
"trust_delta": 3,
|
||||
"world_flags": ["vulcan_ntp_healthy", "vulcan_builds_healthy"],
|
||||
"follow_up_dialogue": "marcus-Q006-complete-clean"
|
||||
},
|
||||
{
|
||||
"id": "ntp-running-not-enabled",
|
||||
"label": "Running But Not Enabled at Boot",
|
||||
"priority": 50,
|
||||
"validation": {
|
||||
"type": "and",
|
||||
"rules": [
|
||||
{ "type": "service_state", "vm": "build_machine", "service": "systemd-timesyncd", "state": "active" },
|
||||
{ "type": "service_enabled", "vm": "build_machine", "service": "systemd-timesyncd", "enabled": false }
|
||||
]
|
||||
},
|
||||
"trust_delta": 1,
|
||||
"world_flags": ["vulcan_ntp_fragile", "vulcan_builds_healthy"],
|
||||
"follow_up_dialogue": "marcus-Q006-complete-fragile"
|
||||
}
|
||||
],
|
||||
"pressure_profile": null,
|
||||
"blast_radius": [],
|
||||
"unlock_requirements": ["world_flag:player_ssh_configured"],
|
||||
"narrative_phase": "unease",
|
||||
"linux_concepts": ["NTP", "systemd-timesyncd", "Arch Linux", "pacman", "package keyring"],
|
||||
"failure_conditions": ["NTP not enabled at boot", "package manager still failing signature checks"],
|
||||
"behavior_impact": {
|
||||
"default": { "curiosity_delta": 0, "obedience_delta": 1, "risk_delta": 0, "suspicion_delta": 0 }
|
||||
},
|
||||
"hidden_hook": null,
|
||||
"access_requirements": {
|
||||
"minimum_access": { "build_machine": "sudo" },
|
||||
"requires_root": false,
|
||||
"temporary_grants_allowed": []
|
||||
},
|
||||
"tags": ["ntp", "time", "pacman", "arch", "build_machine", "services"],
|
||||
"internal_notes": "First quest on vulcan. Introduces Arch Linux and pacman. The clock drift → GPG failure chain is real and genuinely confusing the first time you encounter it. The use of `or` on the NTP objective allows systemd-timesyncd, ntpd, or chronyd — any of them fixes the problem. The explicit check on package installs requires the player to confirm things work, not just that NTP is running."
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"id": "Q007",
|
||||
"title": "Security Theater",
|
||||
"tier": 2,
|
||||
"primary_vm": "web_server",
|
||||
"required_vms": ["workstation", "web_server"],
|
||||
"ticket_id": "T007",
|
||||
"baseline_snapshot": "baseline.post-q004",
|
||||
"summary": "Someone ran a hardening script on hermes that set AllowUsers in sshd_config to only allow a single user: deploy-bot. Now the web-admin group cannot SSH in. Priya filed the ticket after her access was blocked mid-incident response. The AllowUsers directive is correct in intent (locking down SSH) but was applied too aggressively — it needs to include the web-admin group or the relevant users. The player must fix sshd_config and reload sshd without breaking service continuity. Complication: the player must not lock themselves out during the fix, and they must validate that the specific users Priya listed can still SSH.",
|
||||
"clue_fingerprint": {
|
||||
"description": "SSH connection attempts from web-admin accounts fail with 'Permission denied'. sshd_config contains 'AllowUsers deploy-bot' with no other entries. /etc/group shows web-admin group members. The hardening script is in /opt/security/harden-ssh.sh and its log shows it ran last night.",
|
||||
"evidence": [
|
||||
{ "type": "file_contains", "vm": "web_server", "path": "/etc/ssh/sshd_config", "contains": "AllowUsers deploy-bot" },
|
||||
{ "type": "log_contains", "vm": "web_server", "path": "/var/log/auth.log", "contains": "User priya from" },
|
||||
{ "type": "file_exists", "vm": "web_server", "path": "/opt/security/harden-ssh.sh" }
|
||||
]
|
||||
},
|
||||
"objectives": [
|
||||
{
|
||||
"id": "sshd-config-corrected",
|
||||
"description": "sshd_config allows the web-admin group or its members",
|
||||
"check_mode": "passive",
|
||||
"validation": {
|
||||
"type": "or",
|
||||
"rules": [
|
||||
{ "type": "file_contains", "vm": "web_server", "path": "/etc/ssh/sshd_config", "contains": "AllowGroups web-admin" },
|
||||
{ "type": "file_contains", "vm": "web_server", "path": "/etc/ssh/sshd_config", "contains": "priya" }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sshd-still-running",
|
||||
"description": "sshd remains active after config change",
|
||||
"check_mode": "passive",
|
||||
"validation": {
|
||||
"type": "service_state",
|
||||
"vm": "web_server",
|
||||
"service": "sshd",
|
||||
"state": "active"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "deploy-bot-still-allowed",
|
||||
"description": "deploy-bot access is preserved",
|
||||
"check_mode": "passive",
|
||||
"validation": {
|
||||
"type": "or",
|
||||
"rules": [
|
||||
{ "type": "file_contains", "vm": "web_server", "path": "/etc/ssh/sshd_config", "contains": "deploy-bot" },
|
||||
{ "type": "file_contains", "vm": "web_server", "path": "/etc/ssh/sshd_config", "contains": "AllowGroups" }
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"solution_branches": [
|
||||
{
|
||||
"id": "group-based-config",
|
||||
"label": "Proper Fix — Group-Based AllowGroups",
|
||||
"priority": 100,
|
||||
"validation": {
|
||||
"type": "and",
|
||||
"rules": [
|
||||
{ "type": "file_contains", "vm": "web_server", "path": "/etc/ssh/sshd_config", "contains": "AllowGroups web-admin" },
|
||||
{ "type": "service_state", "vm": "web_server", "service": "sshd", "state": "active" },
|
||||
{ "type": "not", "rule": { "type": "file_contains", "vm": "web_server", "path": "/etc/ssh/sshd_config", "contains": "AllowUsers" } }
|
||||
]
|
||||
},
|
||||
"trust_delta": 4,
|
||||
"world_flags": ["hermes_ssh_hardened_correct", "priya_access_restored"],
|
||||
"follow_up_dialogue": "priya-Q007-complete-clean",
|
||||
"follow_up_dialogues": ["marcus-Q007-complete-clean"],
|
||||
"_note": "Best fix. Switches from AllowUsers (fragile, breaks with new users) to AllowGroups (durable, group membership handles access). Trust bump is higher because this is the approach that will scale."
|
||||
},
|
||||
{
|
||||
"id": "allowusers-expanded",
|
||||
"label": "Acceptable Fix — AllowUsers Expanded",
|
||||
"priority": 60,
|
||||
"validation": {
|
||||
"type": "and",
|
||||
"rules": [
|
||||
{ "type": "file_contains", "vm": "web_server", "path": "/etc/ssh/sshd_config", "contains": "priya" },
|
||||
{ "type": "file_contains", "vm": "web_server", "path": "/etc/ssh/sshd_config", "contains": "deploy-bot" },
|
||||
{ "type": "service_state", "vm": "web_server", "service": "sshd", "state": "active" }
|
||||
]
|
||||
},
|
||||
"trust_delta": 1,
|
||||
"world_flags": ["hermes_ssh_allowusers_fragile", "priya_access_restored"],
|
||||
"follow_up_dialogue": "priya-Q007-complete-fragile",
|
||||
"follow_up_dialogues": ["marcus-Q007-complete-fragile"],
|
||||
"_note": "Access is restored but using AllowUsers. Every future new user will need to be manually added. Marcus or Priya will note this later."
|
||||
},
|
||||
{
|
||||
"id": "hardening-removed",
|
||||
"label": "Regression — SSH Restriction Removed Entirely",
|
||||
"priority": 200,
|
||||
"validation": {
|
||||
"type": "and",
|
||||
"rules": [
|
||||
{ "type": "not", "rule": { "type": "file_contains", "vm": "web_server", "path": "/etc/ssh/sshd_config", "contains": "AllowUsers" } },
|
||||
{ "type": "not", "rule": { "type": "file_contains", "vm": "web_server", "path": "/etc/ssh/sshd_config", "contains": "AllowGroups" } },
|
||||
{ "type": "service_state", "vm": "web_server", "service": "sshd", "state": "active" }
|
||||
]
|
||||
},
|
||||
"trust_delta": -3,
|
||||
"world_flags": ["hermes_ssh_unrestricted", "priya_access_restored"],
|
||||
"follow_up_dialogue": "priya-Q007-complete-regression",
|
||||
"follow_up_dialogues": ["marcus-Q007-complete-regression"],
|
||||
"_note": "Player fixed access by removing all restrictions. Priya's access works but the hardening is gone. This is the worst valid outcome — Priya is back in but so is everyone else."
|
||||
}
|
||||
],
|
||||
"pressure_profile": "access_blocked_escalation",
|
||||
"blast_radius": [],
|
||||
"unlock_requirements": ["world_flag:player_ssh_configured"],
|
||||
"narrative_phase": "suspicion",
|
||||
"linux_concepts": ["sshd_config", "AllowGroups", "AllowUsers", "SSH access hardening"],
|
||||
"failure_conditions": ["Priya still locked out", "SSH restrictions removed entirely"],
|
||||
"behavior_impact": {
|
||||
"default": { "curiosity_delta": 1, "obedience_delta": 0, "risk_delta": 0, "suspicion_delta": 0 }
|
||||
},
|
||||
"hidden_hook": {
|
||||
"id": "q007_dale_ssh_key",
|
||||
"description": "An SSH key in hermes /root/.ssh/authorized_keys does not match any current staff. The fingerprint matches no documented key.",
|
||||
"discovery_method": "Player reads /root/.ssh/authorized_keys on hermes",
|
||||
"significance": "Dale had root SSH access to hermes that was never formally revoked."
|
||||
},
|
||||
"access_requirements": {
|
||||
"minimum_access": { "web_server": "sudo" },
|
||||
"requires_root": false,
|
||||
"temporary_grants_allowed": ["sudo:web_server:sshd"]
|
||||
},
|
||||
"tags": ["ssh", "security", "hardening", "sshd", "web_server"],
|
||||
"internal_notes": "This quest introduces Priya as a character and establishes that the player's fixes can have security implications, not just operational ones. The 'regression' branch should feel bad — Priya's grateful but Marcus or a later audit will surface it. The proper fix (AllowGroups) tests whether the player knows the difference between AllowUsers and AllowGroups. The sshd reload vs restart distinction matters here — a player who restarts sshd drops existing connections, which is more disruptive than reload."
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"id": "Q008",
|
||||
"title": "Bad Upstream",
|
||||
"tier": 2,
|
||||
"primary_vm": "web_server",
|
||||
"required_vms": ["workstation", "web_server", "build_machine"],
|
||||
"ticket_id": "T008",
|
||||
"baseline_snapshot": "baseline.post-q006",
|
||||
"summary": "The internal package repository on vulcan is serving a broken version of the axiomworks-app package. A deploy on hermes pulled it in through the internal apt repo and the app is now crashing on startup. The player needs to identify that the problem is in the package (not the app config), trace it back to vulcan, find the broken build artifact, and either roll back the package on hermes or fix the build and republish. This is the first multi-VM quest — investigation crosses from hermes to vulcan.",
|
||||
"clue_fingerprint": {
|
||||
"description": "The app service (axiomworks-app) on hermes is failing. journalctl shows it exits immediately with a non-zero code. The package was updated yesterday via the internal repo at http://vulcan.internal/repo. On vulcan, /srv/repo/axiomworks-app_2.1.1-1_amd64.deb is present but was built from a broken source tarball. The previous version 2.1.0-1 is also in /srv/repo/ and works correctly.",
|
||||
"evidence": [
|
||||
{ "type": "service_state_is", "vm": "web_server", "service": "axiomworks-app", "state": "failed" },
|
||||
{ "type": "log_contains", "vm": "web_server", "path": "/var/log/axiomworks-app.log", "contains": "Exec format error" },
|
||||
{ "type": "file_exists", "vm": "build_machine", "path": "/srv/repo/axiomworks-app_2.1.0-1_amd64.deb" },
|
||||
{ "type": "file_exists", "vm": "build_machine", "path": "/srv/repo/axiomworks-app_2.1.1-1_amd64.deb" }
|
||||
]
|
||||
},
|
||||
"objectives": [
|
||||
{
|
||||
"id": "app-running",
|
||||
"description": "axiomworks-app is active and running",
|
||||
"check_mode": "passive",
|
||||
"validation": {
|
||||
"type": "service_state",
|
||||
"vm": "web_server",
|
||||
"service": "axiomworks-app",
|
||||
"state": "active"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "app-port-listening",
|
||||
"description": "App is accepting connections on expected port",
|
||||
"check_mode": "passive",
|
||||
"validation": {
|
||||
"type": "port_listening",
|
||||
"vm": "web_server",
|
||||
"port": 8080,
|
||||
"protocol": "tcp",
|
||||
"listening": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"solution_branches": [
|
||||
{
|
||||
"id": "rollback-and-pin",
|
||||
"label": "Rollback to 2.1.0 and Pin Version",
|
||||
"priority": 100,
|
||||
"validation": {
|
||||
"type": "and",
|
||||
"rules": [
|
||||
{ "type": "service_state", "vm": "web_server", "service": "axiomworks-app", "state": "active" },
|
||||
{ "type": "port_listening", "vm": "web_server", "port": 8080, "protocol": "tcp", "listening": true },
|
||||
{ "type": "package_installed", "vm": "web_server", "package": "axiomworks-app=2.1.0", "installed": true },
|
||||
{ "type": "file_contains", "vm": "web_server", "path": "/etc/apt/preferences.d/axiomworks-app", "contains": "Pin: version 2.1.0" }
|
||||
]
|
||||
},
|
||||
"trust_delta": 3,
|
||||
"world_flags": ["hermes_app_running", "hermes_app_pinned_2-1-0", "vulcan_bad_build_known"],
|
||||
"follow_up_dialogue": "marcus-Q008-complete-rollback",
|
||||
"follow_up_dialogues": ["sarah-Q008-complete-pinned"],
|
||||
"_note": "Distinguished from rollback-only by an apt pin on hermes. The player must create an apt preferences file after rolling back."
|
||||
},
|
||||
{
|
||||
"id": "rebuild-and-redeploy",
|
||||
"label": "Rebuild on Vulcan and Redeploy",
|
||||
"priority": 80,
|
||||
"validation": {
|
||||
"type": "and",
|
||||
"rules": [
|
||||
{ "type": "service_state", "vm": "web_server", "service": "axiomworks-app", "state": "active" },
|
||||
{ "type": "port_listening", "vm": "web_server", "port": 8080, "protocol": "tcp", "listening": true },
|
||||
{ "type": "package_installed", "vm": "web_server", "package": "axiomworks-app=2.1.1", "installed": true },
|
||||
{ "type": "file_exists", "vm": "build_machine", "path": "/srv/repo/axiomworks-app_2.1.1-2_amd64.deb" }
|
||||
]
|
||||
},
|
||||
"trust_delta": 4,
|
||||
"world_flags": ["hermes_app_running", "vulcan_build_fixed"],
|
||||
"follow_up_dialogue": "marcus-Q008-complete-rebuild",
|
||||
"follow_up_dialogues": ["sarah-Q008-complete-rebuilt"],
|
||||
"_note": "Player fixed the build on vulcan and redeployed the corrected 2.1.1 package. This is the most thorough fix and gets highest trust, but is harder and requires understanding both machines. The rebuilt .deb increments the Debian revision from -1 to -2."
|
||||
},
|
||||
{
|
||||
"id": "rollback-only",
|
||||
"label": "Rollback Only — Version Not Pinned",
|
||||
"priority": 60,
|
||||
"validation": {
|
||||
"type": "and",
|
||||
"rules": [
|
||||
{ "type": "service_state", "vm": "web_server", "service": "axiomworks-app", "state": "active" },
|
||||
{ "type": "port_listening", "vm": "web_server", "port": 8080, "protocol": "tcp", "listening": true },
|
||||
{ "type": "package_installed", "vm": "web_server", "package": "axiomworks-app=2.1.0", "installed": true },
|
||||
{ "type": "not", "rule": { "type": "file_contains", "vm": "web_server", "path": "/etc/apt/preferences.d/axiomworks-app", "contains": "Pin: version 2.1.0" } }
|
||||
]
|
||||
},
|
||||
"trust_delta": 1,
|
||||
"world_flags": ["hermes_app_running", "vulcan_bad_build_known"],
|
||||
"follow_up_incident": "I003",
|
||||
"follow_up_dialogue": "marcus-Q008-complete-unpinned",
|
||||
"follow_up_dialogues": ["sarah-Q008-complete-unpinned"],
|
||||
"_note": "App is running on 2.1.0 but not pinned. No apt preferences pin exists on hermes. The next apt upgrade will pull 2.1.1 back in. I003 re-breaks the app on the next update cycle. The not-rule on the pin file ensures this branch cannot match when rollback-and-pin already matches."
|
||||
}
|
||||
],
|
||||
"pressure_profile": "app_outage_escalation",
|
||||
"blast_radius": ["I003"],
|
||||
"unlock_requirements": [
|
||||
"world_flag:player_ssh_configured",
|
||||
"world_flag:vulcan_ntp_healthy"
|
||||
],
|
||||
"narrative_phase": "suspicion",
|
||||
"linux_concepts": ["apt", "package pinning", "apt preferences", "internal package mirror", "build pipeline"],
|
||||
"failure_conditions": ["axiomworks-app still broken", "bad package not traced to build machine"],
|
||||
"behavior_impact": {
|
||||
"default": { "curiosity_delta": 1, "obedience_delta": 0, "risk_delta": 0, "suspicion_delta": 0 }
|
||||
},
|
||||
"hidden_hook": {
|
||||
"id": "q008_build_log_anomaly",
|
||||
"description": "vulcan's build log for 2.1.1 shows it was triggered by a manual invocation, not the automated pipeline, at 02:14.",
|
||||
"discovery_method": "Player reads /var/log/build-pipeline.log on vulcan and notices the timestamp and manual trigger field",
|
||||
"significance": "The bad build was triggered manually. Someone made the broken build, and it was not the pipeline."
|
||||
},
|
||||
"access_requirements": {
|
||||
"minimum_access": { "build_machine": "sudo", "web_server": "sudo" },
|
||||
"requires_root": false,
|
||||
"temporary_grants_allowed": []
|
||||
},
|
||||
"tags": ["packages", "builds", "multi-vm", "web_server", "build_machine", "deploy"],
|
||||
"internal_notes": "This is the first quest that requires the player to move between two target VMs — hermes and vulcan. The symptom is on hermes but the root cause is on vulcan. Players who don't follow the package trail will spend a long time on hermes looking for a config problem that isn't there. The rebuild branch requires understanding the package build enough to fix the source input and republish a corrected .deb — it's hard but rewarding. The rollback branches are now correctly differentiated: rollback-and-pin requires an apt preferences pin on hermes, and rollback-only explicitly requires its absence via a not-rule."
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"categories": [
|
||||
{
|
||||
"id": "access",
|
||||
"label": "Access & Authentication",
|
||||
"articles": ["ssh-keys", "ssh-access-controls"]
|
||||
},
|
||||
{
|
||||
"id": "web",
|
||||
"label": "Web Services",
|
||||
"articles": ["nginx-config"]
|
||||
},
|
||||
{
|
||||
"id": "storage",
|
||||
"label": "Storage & Logs",
|
||||
"articles": ["disk-logs"]
|
||||
},
|
||||
{
|
||||
"id": "sysadmin",
|
||||
"label": "System Administration",
|
||||
"articles": ["file-permissions", "cron-jobs", "time-sync"]
|
||||
},
|
||||
{
|
||||
"id": "packages",
|
||||
"label": "Package Management",
|
||||
"articles": ["package-management"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"id": "cron-jobs",
|
||||
"title": "Cron Jobs & Scheduled Tasks",
|
||||
"category": "sysadmin",
|
||||
"tags": ["cron", "crontab", "schedule", "backup", "automation"],
|
||||
"updated": "2025-12-01",
|
||||
"summary": "Cron syntax, user vs system crons, and common failure modes.",
|
||||
"sections": [
|
||||
{
|
||||
"heading": "Cron Syntax",
|
||||
"body": "<p>A crontab entry has five time fields followed by the command:</p>",
|
||||
"code": "# ┌─── minute (0–59)\n# │ ┌─── hour (0–23)\n# │ │ ┌─── day of month (1–31)\n# │ │ │ ┌─── month (1–12)\n# │ │ │ │ ┌─── day of week (0–7, 0 and 7 are Sunday)\n# │ │ │ │ │\n * * * * * /path/to/command\n\n# Examples:\n0 2 * * * /usr/local/bin/backup.sh # 2am every day\n*/15 * * * * /usr/local/bin/check.sh # every 15 minutes\n0 0 1 * * /usr/local/bin/monthly.sh # midnight on the 1st"
|
||||
},
|
||||
{
|
||||
"heading": "User Crontabs",
|
||||
"body": "<p>Each user can have their own crontab. Commands run as that user.</p>",
|
||||
"code": "crontab -e # edit your crontab\ncrontab -l # list your crontab\ncrontab -l -u alice # list alice's crontab (root only)\ncrontab -r # delete your crontab (dangerous—no confirmation)"
|
||||
},
|
||||
{
|
||||
"heading": "System Cron Directories",
|
||||
"body": "<p>Scripts dropped into these directories run at the corresponding interval without needing a crontab entry:</p>",
|
||||
"code": "/etc/cron.daily/\n/etc/cron.weekly/\n/etc/cron.monthly/\n/etc/cron.hourly/\n\n# Scripts here must be executable and owned by root.\n# They must NOT have a file extension—run-parts ignores files with dots in the name."
|
||||
},
|
||||
{
|
||||
"heading": "Ownership and the PATH Problem",
|
||||
"body": "<p>Two common failure modes:</p><p><strong>Wrong owner:</strong> A cron script in <code>/etc/cron.daily/</code> must be owned by root. If it is owned by another user, run-parts may skip it.</p><p><strong>Missing PATH:</strong> Cron does not source <code>.bashrc</code> or <code>.profile</code>. Commands that work interactively may fail in cron because the PATH only contains <code>/usr/bin:/bin</code>. Always use full paths in cron scripts, or set PATH explicitly at the top of the script.</p>",
|
||||
"code": "#!/bin/bash\nPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\n..."
|
||||
},
|
||||
{
|
||||
"heading": "Checking If a Cron Ran",
|
||||
"body": "",
|
||||
"code": "# Check syslog or the cron-specific log\ngrep CRON /var/log/syslog | tail -20\ncat /var/log/cron.log # if separate cron log is configured\n\n# Check journald\njournalctl -u cron --since \"1 hour ago\""
|
||||
},
|
||||
{
|
||||
"heading": "Capturing Cron Output",
|
||||
"body": "<p>By default, cron mails output to the user. On servers with no mail configured, errors disappear silently. Redirect to a log file instead:</p>",
|
||||
"code": "0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"id": "disk-logs",
|
||||
"title": "Disk Space & Log Rotation",
|
||||
"category": "storage",
|
||||
"tags": ["disk", "df", "du", "logs", "logrotate", "cleanup"],
|
||||
"updated": "2025-08-22",
|
||||
"summary": "Finding what is filling the disk and keeping logs from growing unbounded.",
|
||||
"sections": [
|
||||
{
|
||||
"heading": "Checking Disk Usage",
|
||||
"body": "<p><code>df</code> shows you how full each filesystem is. <code>du</code> tells you where the space went.</p>",
|
||||
"code": "df -h # human-readable filesystem summary\ndf -h /var/log # check a specific mount\n\ndu -sh /var/log/* # top-level breakdown of /var/log\ndu -sh /var/* | sort -rh # sort by size, largest first\ndu -sh /var/log/*.log # sizes of individual log files"
|
||||
},
|
||||
{
|
||||
"heading": "Finding Large Files",
|
||||
"body": "<p>When du does not point at an obvious culprit:</p>",
|
||||
"code": "# Files over 100MB anywhere on the system\nfind / -xdev -size +100M -type f 2>/dev/null\n\n# Files in /var that have grown recently\nfind /var -xdev -mtime -1 -size +10M -type f 2>/dev/null"
|
||||
},
|
||||
{
|
||||
"heading": "Emergency Cleanup",
|
||||
"body": "<p>If disk is at 100% and a service is failing because of it:</p>",
|
||||
"code": "# Truncate a log file without deleting it (safe for running processes)\ntruncate -s 0 /var/log/nginx/access.log\n\n# Remove old compressed logs (the .gz files are already rotated)\nrm /var/log/nginx/*.gz\n\n# Clear journald logs older than 2 days\njournalctl --vacuum-time=2d"
|
||||
},
|
||||
{
|
||||
"heading": "logrotate Basics",
|
||||
"body": "<p>logrotate is the standard tool for rotating and compressing logs on a schedule. It is usually run daily from cron. Config files live in <code>/etc/logrotate.d/</code>—one file per service.</p>"
|
||||
},
|
||||
{
|
||||
"heading": "Writing a logrotate Config",
|
||||
"body": "<p>Example for an nginx access log:</p>",
|
||||
"code": "/var/log/nginx/access.log {\n daily\n rotate 14\n compress\n delaycompress\n missingok\n notifempty\n sharedscripts\n postrotate\n /bin/kill -USR1 $(cat /run/nginx.pid 2>/dev/null) 2>/dev/null || true\n endscript\n}"
|
||||
},
|
||||
{
|
||||
"heading": "Testing logrotate",
|
||||
"body": "<p>Run logrotate manually in debug mode to verify a config without actually rotating anything:</p>",
|
||||
"code": "logrotate -d /etc/logrotate.d/nginx\n\n# To force a rotation right now (useful for testing):\nlogrotate -f /etc/logrotate.d/nginx"
|
||||
},
|
||||
{
|
||||
"heading": "Key logrotate Directives",
|
||||
"body": "<table><tr><th>Directive</th><th>Meaning</th></tr><tr><td><code>daily/weekly/monthly</code></td><td>Rotation frequency</td></tr><tr><td><code>rotate N</code></td><td>Keep N old copies</td></tr><tr><td><code>compress</code></td><td>gzip old files</td></tr><tr><td><code>delaycompress</code></td><td>Skip compressing the most recent rotation (useful when the app still has it open)</td></tr><tr><td><code>missingok</code></td><td>Do not error if the log file does not exist</td></tr><tr><td><code>notifempty</code></td><td>Skip rotation if the file is empty</td></tr><tr><td><code>size 100M</code></td><td>Rotate when file exceeds this size instead of on schedule</td></tr></table>"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"id": "file-permissions",
|
||||
"title": "File Ownership & Permissions",
|
||||
"category": "sysadmin",
|
||||
"tags": ["chown", "chmod", "permissions", "ownership", "ls"],
|
||||
"updated": "2025-10-07",
|
||||
"summary": "Understanding and fixing file ownership and permission bits.",
|
||||
"sections": [
|
||||
{
|
||||
"heading": "Reading the Permission String",
|
||||
"body": "<p>Run <code>ls -l</code> to see permissions. The first column looks like <code>-rwxr-xr--</code>.</p><ul><li>First character: <code>-</code> file, <code>d</code> directory, <code>l</code> symlink</li><li>Next three: owner read/write/execute</li><li>Next three: group read/write/execute</li><li>Last three: others read/write/execute</li></ul><p><code>r</code>=4, <code>w</code>=2, <code>x</code>=1. Add them up for octal notation: <code>rwx</code>=7, <code>rw-</code>=6, <code>r--</code>=4.</p>"
|
||||
},
|
||||
{
|
||||
"heading": "chown — Changing Ownership",
|
||||
"body": "<p>Change the owner and/or group of a file or directory.</p>",
|
||||
"code": "chown user file # change owner only\nchown user:group file # change owner and group\nchown :group file # change group only\n\n# Recursive — change everything under a directory\nchown -R user:group /path/to/dir"
|
||||
},
|
||||
{
|
||||
"heading": "chmod — Changing Permissions",
|
||||
"body": "",
|
||||
"code": "chmod 644 file.txt # rw-r--r-- (typical for files)\nchmod 755 /usr/local/bin/app # rwxr-xr-x (typical for executables)\nchmod 700 ~/.ssh # rwx------ (private directory)\nchmod 600 ~/.ssh/authorized_keys # rw------- (private file)\n\n# Recursive\nchmod -R 755 /var/www/html\n\n# Symbolic form (add execute for owner only)\nchmod u+x script.sh"
|
||||
},
|
||||
{
|
||||
"heading": "Common Patterns",
|
||||
"body": "<table><tr><th>Mode</th><th>Numeric</th><th>Typical use</th></tr><tr><td><code>rw-r--r--</code></td><td>644</td><td>Regular files, config files</td></tr><tr><td><code>rwxr-xr-x</code></td><td>755</td><td>Directories, executables</td></tr><tr><td><code>rwx------</code></td><td>700</td><td>Private directories (e.g. ~/.ssh)</td></tr><tr><td><code>rw-------</code></td><td>600</td><td>Private files (e.g. private keys, authorized_keys)</td></tr><tr><td><code>rwxrwxr-x</code></td><td>775</td><td>Shared directories where the group needs write access</td></tr></table>"
|
||||
},
|
||||
{
|
||||
"heading": "Checking Who Owns What",
|
||||
"body": "",
|
||||
"code": "ls -la /var/www/html # list with ownership\nstat file.txt # detailed file metadata\nfind /path -user root # find files owned by root\nfind /path -not -user deploy # find files NOT owned by deploy"
|
||||
},
|
||||
{
|
||||
"heading": "A Note on Recursive chown",
|
||||
"body": "<p>When you run <code>chown -R</code>, it changes <em>everything</em> under the path—including files and subdirectories that may have intentionally different ownership. Know what you are targeting before running it on a live system. Check with <code>ls -laR</code> or <code>find</code> first.</p>"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "nginx-config",
|
||||
"title": "nginx Configuration",
|
||||
"category": "web",
|
||||
"tags": ["nginx", "config", "syntax", "reload", "vhost"],
|
||||
"updated": "2025-09-18",
|
||||
"summary": "nginx config structure, common syntax errors, and safe reload procedure.",
|
||||
"sections": [
|
||||
{
|
||||
"heading": "Config File Layout",
|
||||
"body": "<p>nginx uses a block-based config syntax. The main file is <code>/etc/nginx/nginx.conf</code>. Site configs live in <code>/etc/nginx/sites-available/</code> and are symlinked into <code>/etc/nginx/sites-enabled/</code> to activate them.</p><p>Every block opens with <code>{</code> and closes with <code>}</code>. Every directive ends with <code>;</code>. Missing either one will fail the syntax check.</p>"
|
||||
},
|
||||
{
|
||||
"heading": "Testing Config Before Reloading",
|
||||
"body": "<p>Always test before reloading. A bad config will prevent nginx from reloading, but it will <em>not</em> take down the running process—the old config stays live.</p>",
|
||||
"code": "nginx -t\n# or\nnginx -T # prints the full parsed config"
|
||||
},
|
||||
{
|
||||
"heading": "Reloading vs Restarting",
|
||||
"body": "<p>Use reload, not restart. Reload applies the new config without dropping existing connections.</p>",
|
||||
"code": "systemctl reload nginx\n\n# Only use restart if you have to—it drops active connections.\nsystemctl restart nginx"
|
||||
},
|
||||
{
|
||||
"heading": "Common Syntax Errors",
|
||||
"body": "<ul><li>Missing semicolon at the end of a directive</li><li>Missing closing brace <code>}</code> on a block</li><li>Typo in a directive name (nginx will report \"unknown directive\")</li><li>Referencing a cert file or log path that does not exist</li><li>Duplicate <code>listen</code> directives on the same port across multiple vhosts without <code>default_server</code> resolution</li></ul><p>The error message from <code>nginx -t</code> includes the file name and line number. Read it.</p>"
|
||||
},
|
||||
{
|
||||
"heading": "Useful Log Paths",
|
||||
"body": "<p>Default paths on Debian/Ubuntu:</p>",
|
||||
"code": "/var/log/nginx/error.log\n/var/log/nginx/access.log\n\n# Per-vhost logs are usually defined in the server block:\naccess_log /var/log/nginx/mysite.access.log;\nerror_log /var/log/nginx/mysite.error.log;"
|
||||
},
|
||||
{
|
||||
"heading": "Quick Vhost Template",
|
||||
"body": "<p>Minimal working vhost for a static site:</p>",
|
||||
"code": "server {\n listen 80;\n server_name example.internal;\n\n root /var/www/example;\n index index.html;\n\n location / {\n try_files $uri $uri/ =404;\n }\n\n access_log /var/log/nginx/example.access.log;\n error_log /var/log/nginx/example.error.log;\n}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"id": "package-management",
|
||||
"title": "Package Management & Version Pinning",
|
||||
"category": "packages",
|
||||
"tags": ["apt", "pacman", "packages", "pinning", "rollback", "IgnorePkg"],
|
||||
"updated": "2026-01-08",
|
||||
"summary": "Installing, rolling back, and pinning packages on Debian and Arch Linux.",
|
||||
"sections": [
|
||||
{
|
||||
"heading": "Debian / Ubuntu (apt)",
|
||||
"body": "<p>Most commands need root.</p>",
|
||||
"code": "apt update # refresh package list\napt install nginx # install\napt remove nginx # remove (keep config)\napt purge nginx # remove + delete config\napt list --installed # list installed packages\napt show nginx # info about a package\ndpkg -l | grep nginx # alternative listing"
|
||||
},
|
||||
{
|
||||
"heading": "Listing Available Versions (Debian)",
|
||||
"body": "",
|
||||
"code": "apt-cache policy nginx\n# Shows installed version, candidate version, and all available versions by priority"
|
||||
},
|
||||
{
|
||||
"heading": "Installing a Specific Version (Debian)",
|
||||
"body": "",
|
||||
"code": "apt install nginx=1.22.1-9\n# Use apt-cache policy to find the exact version string first"
|
||||
},
|
||||
{
|
||||
"heading": "Pinning a Package (Debian)",
|
||||
"body": "<p>Pinning prevents apt from upgrading a specific package. Create or edit <code>/etc/apt/preferences.d/</code>:</p>",
|
||||
"code": "# /etc/apt/preferences.d/nginx-pin\nPackage: nginx\nPin: version 1.22.1-9\nPin-Priority: 1001\n\n# Priority > 1000 = keep this version even if newer is available\n# After creating the file:\napt-mark hold nginx # belt-and-suspenders hold\napt-cache policy nginx # verify the pin took effect"
|
||||
},
|
||||
{
|
||||
"heading": "Arch Linux (pacman)",
|
||||
"body": "",
|
||||
"code": "pacman -Syu # update all\npacman -S nginx # install\npacman -R nginx # remove\npacman -Rs nginx # remove + unneeded deps\npacman -Q | grep nginx # list installed\npacman -Qi nginx # info about installed package"
|
||||
},
|
||||
{
|
||||
"heading": "Rolling Back a Package (Arch)",
|
||||
"body": "<p>Arch keeps a package cache in <code>/var/cache/pacman/pkg/</code>. If the current package broke something:</p>",
|
||||
"code": "ls /var/cache/pacman/pkg/nginx*\n# Find the version you want, then:\npacman -U /var/cache/pacman/pkg/nginx-1.24.0-1-x86_64.pkg.tar.zst"
|
||||
},
|
||||
{
|
||||
"heading": "Preventing Upgrades (Arch — IgnorePkg)",
|
||||
"body": "<p>After rolling back, prevent the package from upgrading on the next <code>pacman -Syu</code>:</p>",
|
||||
"code": "# /etc/pacman.conf\n[options]\n...\nIgnorePkg = nginx\n\n# Verify:\npacman -Syu\n# Should print: warning: nginx: ignoring package upgrade (1.24.0-1 => 1.25.x-y)"
|
||||
},
|
||||
{
|
||||
"heading": "When to Pin vs When to Fix",
|
||||
"body": "<p>Pinning is a stop-gap, not a solution. Document why you pinned it and set a reminder to revisit. A pinned package stops receiving security updates. If the upstream bug is fixed in a newer minor version, upgrade to that instead of staying pinned indefinitely.</p>"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"id": "ssh-access-controls",
|
||||
"title": "SSH Server Access Controls",
|
||||
"category": "access",
|
||||
"tags": ["ssh", "sshd_config", "AllowUsers", "AllowGroups", "security", "hardening"],
|
||||
"updated": "2025-10-29",
|
||||
"summary": "Restricting who can SSH in using sshd_config directives.",
|
||||
"sections": [
|
||||
{
|
||||
"heading": "The Config File",
|
||||
"body": "<p>SSH server configuration lives in <code>/etc/ssh/sshd_config</code>. Drop-in overrides can go in <code>/etc/ssh/sshd_config.d/*.conf</code>.</p><p><strong>Always test your config before reloading:</strong></p>",
|
||||
"code": "sshd -t\n# If it prints nothing and exits 0, the config is valid.\nsystemctl reload ssh"
|
||||
},
|
||||
{
|
||||
"heading": "AllowUsers and AllowGroups",
|
||||
"body": "<p>These are whitelist directives. If either is set, only matching users or group members can log in. If neither is set, all users may try.</p>",
|
||||
"code": "# Only these users may log in\nAllowUsers alice bob deploy\n\n# Only members of these groups may log in\nAllowGroups sshusers ops\n\n# Combining: user must match AllowUsers AND (if AllowGroups is set) be in an allowed group\n# These are independent filters—if both are set, a user must satisfy both."
|
||||
},
|
||||
{
|
||||
"heading": "DenyUsers and DenyGroups",
|
||||
"body": "<p>Blacklist alternatives. <code>DenyUsers</code> and <code>DenyGroups</code> are checked before Allow rules.</p><p>Prefer <code>AllowUsers</code>/<code>AllowGroups</code> over Deny lists—it is safer to enumerate who <em>can</em> in rather than who cannot.</p>"
|
||||
},
|
||||
{
|
||||
"heading": "Other Common Restrictions",
|
||||
"body": "",
|
||||
"code": "# Disable root login entirely (recommended)\nPermitRootLogin no\n\n# Disable password authentication (once keys are working)\nPasswordAuthentication no\n\n# Change the listening port (minor obscurity, not real security)\nPort 2222\n\n# Restrict to specific network interface\nListenAddress 10.42.0.1\n\n# Idle session timeout (seconds × count before disconnect)\nClientAliveInterval 300\nClientAliveCountMax 2"
|
||||
},
|
||||
{
|
||||
"heading": "Match Blocks",
|
||||
"body": "<p>You can apply different rules to specific users, groups, or source addresses:</p>",
|
||||
"code": "# Allow password auth only from the management network\nMatch Address 10.42.0.0/24\n PasswordAuthentication yes\n\n# Give one user a restricted shell\nMatch User backup-agent\n ForceCommand /usr/local/bin/backup-only\n AllowTcpForwarding no"
|
||||
},
|
||||
{
|
||||
"heading": "Checking Who Has Access",
|
||||
"body": "<p>There is no built-in command to list all users who currently satisfy the access rules. Check manually:</p>",
|
||||
"code": "# Current AllowUsers/AllowGroups settings\ngrep -iE '(AllowUsers|AllowGroups|DenyUsers|DenyGroups)' /etc/ssh/sshd_config\n\n# Members of a group\ngetent group sshusers\n\n# All users with a valid shell (can SSH in if no restrictions)\ngrep -v '/nologin\\|/false' /etc/passwd"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "ssh-keys",
|
||||
"title": "SSH Key Authentication",
|
||||
"category": "access",
|
||||
"tags": ["ssh", "authorized_keys", "keys", "permissions"],
|
||||
"updated": "2025-11-03",
|
||||
"summary": "How SSH key auth works and how to set it up correctly.",
|
||||
"sections": [
|
||||
{
|
||||
"heading": "How It Works",
|
||||
"body": "<p>SSH key authentication replaces passwords with a cryptographic key pair. The <strong>private key</strong> stays on your machine. The <strong>public key</strong> goes into <code>~/.ssh/authorized_keys</code> on the target host. When you connect, the server checks whether your private key corresponds to one of the public keys it trusts.</p><p>There is no password transmitted. Either the key matches or the connection fails.</p>"
|
||||
},
|
||||
{
|
||||
"heading": "Generating a Key Pair",
|
||||
"body": "<p>Use <code>ed25519</code> unless something forces you onto RSA. It is smaller and more secure.</p>",
|
||||
"code": "ssh-keygen -t ed25519 -C \"your-comment-here\"\n# Accept the default path (~/.ssh/id_ed25519) or specify one.\n# Passphrase is optional but recommended for keys that leave your machine."
|
||||
},
|
||||
{
|
||||
"heading": "Installing the Public Key",
|
||||
"body": "<p>Copy the public key to the remote host:</p>",
|
||||
"code": "# Option 1 — if password auth is still working\nssh-copy-id -i ~/.ssh/id_ed25519.pub user@host\n\n# Option 2 — manually\ncat ~/.ssh/id_ed25519.pub >> ~/.ssh/authorized_keys"
|
||||
},
|
||||
{
|
||||
"heading": "File and Directory Permissions",
|
||||
"body": "<p>This is the most common reason key auth fails. SSH will silently reject keys if the permissions are too open.</p>",
|
||||
"code": "chmod 700 ~/.ssh\nchmod 600 ~/.ssh/authorized_keys\nchown -R youruser:youruser ~/.ssh"
|
||||
},
|
||||
{
|
||||
"heading": "Troubleshooting",
|
||||
"body": "<p>Run <code>ssh -v user@host</code> for verbose output. The auth failure reason is usually in the first 20 lines.</p><p>Common causes:</p><ul><li><code>authorized_keys</code> file has wrong permissions (see above)</li><li><code>~/.ssh</code> directory is world-writable</li><li><code>authorized_keys</code> file does not exist</li><li>The file exists but is empty or the key was pasted with a line break in the middle</li><li><code>sshd_config</code> has <code>PubkeyAuthentication no</code></li></ul>"
|
||||
},
|
||||
{
|
||||
"heading": "Checking the sshd Config",
|
||||
"body": "<p>Relevant lines in <code>/etc/ssh/sshd_config</code>:</p>",
|
||||
"code": "PubkeyAuthentication yes\nAuthorizedKeysFile .ssh/authorized_keys\n\n# After editing sshd_config, test before reloading:\nsshd -t\nsystemctl reload ssh"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"id": "time-sync",
|
||||
"title": "System Time & NTP",
|
||||
"category": "sysadmin",
|
||||
"tags": ["ntp", "time", "timedatectl", "timesyncd", "chrony", "drift"],
|
||||
"updated": "2025-07-14",
|
||||
"summary": "Keeping system clocks accurate and diagnosing time drift.",
|
||||
"sections": [
|
||||
{
|
||||
"heading": "Why System Time Matters",
|
||||
"body": "<p>Clocks that drift cause more problems than you expect: SSL certificate validation failures, log timestamps that do not correlate across machines, cron jobs that fire at the wrong time, authentication tokens that expire prematurely, and package signature checks that fail.</p><p>On a server, time should be correct to within a second. Most NTP implementations keep it within milliseconds.</p>"
|
||||
},
|
||||
{
|
||||
"heading": "Checking Current Time Status",
|
||||
"body": "",
|
||||
"code": "timedatectl\n# Shows: local time, UTC time, timezone, NTP sync status, RTC time\n\ntimedatectl show\n# Machine-readable version of the same"
|
||||
},
|
||||
{
|
||||
"heading": "systemd-timesyncd",
|
||||
"body": "<p>Most Debian/Ubuntu systems ship with <code>systemd-timesyncd</code> as the default NTP client. It is a lightweight SNTP implementation—adequate for most servers.</p>",
|
||||
"code": "# Enable and start\nsystemctl enable --now systemd-timesyncd\n\n# Check sync status\ntimedatectl timesync-status\n\n# Force a resync\nsystemctl restart systemd-timesyncd\n\n# Config file (NTP servers, fallback)\ncat /etc/systemd/timesyncd.conf"
|
||||
},
|
||||
{
|
||||
"heading": "NTP Server Configuration",
|
||||
"body": "<p>The default NTP servers are usually fine. If you need to change them—for example, to use an internal NTP server:</p>",
|
||||
"code": "# /etc/systemd/timesyncd.conf\n[Time]\nNTP=ntp.internal.example.com\nFallbackNTP=0.debian.pool.ntp.org 1.debian.pool.ntp.org"
|
||||
},
|
||||
{
|
||||
"heading": "chrony (alternative)",
|
||||
"body": "<p>chrony is a more capable NTP implementation. It handles intermittent network connections and large initial offsets better than timesyncd. On systems where accuracy matters:</p>",
|
||||
"code": "apt install chrony\nsystemctl enable --now chrony\n\nchronyc tracking # current sync status\nchronyc sources -v # configured time sources and their offsets"
|
||||
},
|
||||
{
|
||||
"heading": "Diagnosing Time Problems",
|
||||
"body": "",
|
||||
"code": "# Is NTP enabled?\ntimedatectl | grep NTP\n\n# Is timesyncd active?\nsystemctl status systemd-timesyncd\n\n# Did a sync happen recently?\njournalctl -u systemd-timesyncd --since \"1 hour ago\"\n\n# What is the current offset?\ntimedatectl timesync-status | grep Offset"
|
||||
},
|
||||
{
|
||||
"heading": "Setting Timezone",
|
||||
"body": "",
|
||||
"code": "timedatectl list-timezones | grep Europe\ntimedatectl set-timezone Europe/London"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "T001",
|
||||
"from": "Marcus Webb <m.webb@axiomworks.internal>",
|
||||
"subject": "Your workstation access",
|
||||
"body": "Hey, welcome to the team. HR said you started today so I got you set up with an account on ares. The provisioning script runs automatically but it does not handle SSH keys — you will need to add yours manually. Your public key should be in the onboarding doc. Let me know if you get stuck.\n\n— Marcus",
|
||||
"initial_priority": "low",
|
||||
"current_priority": "low",
|
||||
"target_vm": "workstation",
|
||||
"linked_quest": "Q001",
|
||||
"tags": ["onboarding", "ssh", "workstation"],
|
||||
"deadline_behavior": "none",
|
||||
"attachments": ["docs/onboarding.json"]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "T002",
|
||||
"from": "Sarah Chen <s.chen@axiomworks.internal>",
|
||||
"subject": "[prod-web] site is down",
|
||||
"body": "Getting connection refused on the main site. Started about 20 minutes ago. Nothing changed on our end as far as I know.",
|
||||
"initial_priority": "high",
|
||||
"current_priority": "high",
|
||||
"target_vm": "web_server",
|
||||
"linked_quest": "Q002",
|
||||
"tags": ["services", "web", "nginx"],
|
||||
"deadline_behavior": "escalates",
|
||||
"follow_up_ticket_ids": ["T002-followup"]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "T003-recurrence",
|
||||
"from": "Monitoring <alerts@axiomworks.internal>",
|
||||
"subject": "disk pressure returned on hermes",
|
||||
"body": "Disk pressure has returned on hermes. /var/log/nginx/access.log is growing again and the host is trending back toward saturation.",
|
||||
"initial_priority": "high",
|
||||
"current_priority": "high",
|
||||
"target_vm": "web_server",
|
||||
"linked_quest": "Q003",
|
||||
"tags": ["web", "disk", "nginx", "recurrence"],
|
||||
"deadline_behavior": "escalates",
|
||||
"_note": "Recurrence ticket emitted by I001 when the earlier partial fix allows log pressure to return."
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "T003",
|
||||
"from": "Dave Okonkwo <d.okonkwo@axiomworks.internal>",
|
||||
"subject": "is the website slow for anyone else",
|
||||
"body": "Pages are loading really slowly for me. Sometimes they time out. I rebooted my laptop but it did not help. Is something wrong on the server side?",
|
||||
"initial_priority": "medium",
|
||||
"current_priority": "medium",
|
||||
"target_vm": "web_server",
|
||||
"linked_quest": "Q003",
|
||||
"tags": ["web", "disk", "nginx"],
|
||||
"deadline_behavior": "escalates",
|
||||
"_note": "Dave is reporting symptoms of the disk being nearly full causing nginx write failures and slowdowns. He thinks it's a network issue. He is wrong but his symptom report is accurate."
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "T004",
|
||||
"from": "Sarah Chen <s.chen@axiomworks.internal>",
|
||||
"subject": "deployment not applying",
|
||||
"body": "I pushed a change this morning and the site is still showing the old version. I confirmed the deploy script ran and it said it completed successfully. But the file timestamp on the server doesn't match what I deployed. Did something change in how deploys work?",
|
||||
"initial_priority": "medium",
|
||||
"current_priority": "medium",
|
||||
"target_vm": "web_server",
|
||||
"linked_quest": "Q004",
|
||||
"tags": ["deploy", "permissions", "web_server"],
|
||||
"deadline_behavior": "none",
|
||||
"_note": "Sarah correctly identifies the symptom but assumes the script is at fault. The script is fine. The permissions are the problem. Her description of the deploy 'completing successfully' is accurate — the script ran, it just could not overwrite root-owned files and silently skipped them."
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"id": "T005",
|
||||
"from": "Dave Okonkwo <d.okonkwo@axiomworks.internal>",
|
||||
"subject": "disk warning on hermes again",
|
||||
"body": "Got an alert that /var/backups is at 85%. I don't know if this is related to what was going on before. Probably fine but figured you should know.",
|
||||
"initial_priority": "low",
|
||||
"current_priority": "low",
|
||||
"target_vm": "web_server",
|
||||
"linked_quest": "Q005",
|
||||
"tags": [
|
||||
"disk",
|
||||
"backup",
|
||||
"web_server"
|
||||
],
|
||||
"deadline_behavior": "escalates"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"id": "T006",
|
||||
"from": "Dave Okonkwo <d.okonkwo@axiomworks.internal>",
|
||||
"subject": "builds failing on vulcan",
|
||||
"body": "Getting signature errors every time I try to install anything on the build machine. Tried pacman -Syu and it fails partway through. I didn't change anything. It was working yesterday.",
|
||||
"initial_priority": "medium",
|
||||
"current_priority": "medium",
|
||||
"target_vm": "build_machine",
|
||||
"linked_quest": "Q006",
|
||||
"tags": [
|
||||
"pacman",
|
||||
"build_machine",
|
||||
"packages"
|
||||
],
|
||||
"deadline_behavior": "none"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"id": "T007",
|
||||
"from": "Priya Nair <p.nair@axiomworks.internal>",
|
||||
"subject": "locked out of hermes",
|
||||
"body": "I cannot SSH into hermes. Permission denied immediately. I was in the middle of something. Who ran a hardening script without telling anyone.",
|
||||
"initial_priority": "critical",
|
||||
"current_priority": "critical",
|
||||
"target_vm": "web_server",
|
||||
"linked_quest": "Q007",
|
||||
"tags": [
|
||||
"ssh",
|
||||
"access",
|
||||
"web_server",
|
||||
"security"
|
||||
],
|
||||
"deadline_behavior": "escalates"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "T008",
|
||||
"from": "Sarah Chen <s.chen@axiomworks.internal>",
|
||||
"subject": "app is down after update",
|
||||
"body": "The deploy ran this morning and now the app won't start. It's returning nothing on 8080. The update pulled in a new package version. I don't know if that's the problem but the timing is suspicious.",
|
||||
"initial_priority": "high",
|
||||
"current_priority": "high",
|
||||
"target_vm": "web_server",
|
||||
"linked_quest": "Q008",
|
||||
"tags": [
|
||||
"app",
|
||||
"deploy",
|
||||
"packages",
|
||||
"web_server"
|
||||
],
|
||||
"deadline_behavior": "escalates",
|
||||
"_note": "Sarah correctly suspects the package update. She doesn't know the build machine is involved."
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"id": "build_machine",
|
||||
"domain": "sc-build-machine",
|
||||
"hostname": "vulcan",
|
||||
"distro": "arch",
|
||||
"role": "Build/package/update quest target VM",
|
||||
"display_name": "Build Machine (vulcan)",
|
||||
"profile_type": "headless_server",
|
||||
"resource_budget": {
|
||||
"ram_mb": 384,
|
||||
"vcpus": 2,
|
||||
"disk_gb": 10,
|
||||
"note": "Slightly more CPU for build tasks. Still headless."
|
||||
},
|
||||
"network": {
|
||||
"mode": "quest",
|
||||
"libvirt_network": "sc-internal",
|
||||
"optional_outbound": "sc-pkg-mirror",
|
||||
"note": "Selective outbound access to package mirror for update quests."
|
||||
},
|
||||
"ssh_user": "player",
|
||||
"ssh_key": "~/.ssh/sc_host_key",
|
||||
"snapshots": {
|
||||
"baseline": "baseline.clean",
|
||||
"recovery": "baseline.recovery",
|
||||
"checkpoint_prefix": "checkpoint.shift-",
|
||||
"max_checkpoints": 5
|
||||
},
|
||||
"guest_helper": {
|
||||
"name": "ops-telemetry-cache",
|
||||
"path": "/usr/local/bin/ops-telemetry-cache",
|
||||
"trusted": false
|
||||
},
|
||||
"display": {
|
||||
"type": "vnc",
|
||||
"fallback": "spice"
|
||||
},
|
||||
"always_live": false,
|
||||
"quests": ["Q006", "Q008"],
|
||||
"note": "Arch Linux build machine. Named vulcan — the forge. Handles package/build/update quests."
|
||||
}
|
||||