ADR-0007: Lisp-Scripted Mission FFI & Contract Model
Supersedes spike: former spikes/ADR-0002-scripted-mission-ffi.md
Context: ADR-0002 enables scripted missions — advanced missions that require players to write Lisp code to orchestrate across cartridges. This spike defines the FFI subset available in mission context and formalizes the acceptance-contract format.
Context
Section titled “Context”What: A scripted mission is a mission that requires the player to author a Lisp expression that, when evaluated in the mission context, produces a result satisfying a predicate.
Why: Enables puzzle-solving-via-code, cross-cartridge orchestration, and accessible challenge pacing (easier than freeform coding, harder than button-pushing).
Where: Scripted missions are delivered in three ways:
- Single-cartridge: “Write a filter function that selects nodes by threat level.”
- Multi-capability: “Write a function that chains ICE Breaker reconnaissance with BLACK LEDGER transaction tracing.”
- Free-play REPL: Player writes and executes code in the nOSh runtime REPL for learning.
Restricted FFI Surface
Section titled “Restricted FFI Surface”Principle: Scripted missions run in a sandboxed Lisp context. Not all NoshAPI is available. Access is restricted by mission author intent.
Tier 1: Always Available (All Scripted Missions)
Section titled “Tier 1: Always Available (All Scripted Missions)”Pure computation:
(+ 1 2) ; Arithmetic(> x 5) ; Comparison(if condition true-val false-val) ; Conditional(map fn list) ; Higher-order(filter fn list) ; Higher-order(reduce fn init list) ; Fold(let ((x 1)) body) ; Binding(lambda (x) body) ; Functions(quote x) ; Quoting(car x) ; List ops(cdr x)(cons x y)(null? x)(list a b c) ; List constructionString/type operations:
(string-append s1 s2) ; String concatenation(string-length s)(string-ref s i)(number->string n)(symbol->string sym)Mission-local data access:
(mission-input) ; Input provided by mission template(mission-context) ; Local mission state (read-only)Debug output (educational, not for mission verification):
(print x) ; Log to mission console(describe x) ; Type introspectionTier 2: Granted by Mission Author (Optional Capabilities)
Section titled “Tier 2: Granted by Mission Author (Optional Capabilities)”Conditional on mission template’s:grants clause:
(cartridge-data :ice-breaker) ; Read from loaded cartridge state (if mission grants access)(current-mission-phase) ; Phase metadata(mission-deck-state) ; Operator reputation, credits, history (read-only)(random seed) ; PRNG (if mission author enables it)Tier 3: Forbidden (Never Available)
Section titled “Tier 3: Forbidden (Never Available)”No side effects outside the mission context:
(credit-add amount) ; FORBIDDEN — affects global deck state(rep-modify delta) ; FORBIDDEN(spawn-cell ...) ; FORBIDDEN — cell creation is cartridge-only(nosh-text-puts ...) ; FORBIDDEN — display is mission framework's job(sfx-confirm ...) ; FORBIDDEN — sound is framework's job(cart-save state) ; FORBIDDEN — persistence is framework's jobNo access to other cartridges’ internal state:
(black-ledger-transaction-list) ; FORBIDDEN — inter-cartridge leakage(neongrid-current-position) ; FORBIDDENNo reflection/metaprogramming:
(eval ...) ; FORBIDDEN — dynamic code eval(load-file ...) ; FORBIDDEN(intern symbol) ; FORBIDDENMission-Local State Model
Section titled “Mission-Local State Model”Each scripted mission runs in an isolated arena with its own state.
Input Contract
Section titled “Input Contract”Provided by mission template:
(:input (() . input-value)) ; A Lisp value passed to the scriptExample: ICE Breaker extraction mission provides:
(mission-input); => (struct scan-result; (nodes (list node-1 node-2 node-3)); (threat-level 3); (time-limit 8))Output Contract
Section titled “Output Contract”Player script returns:
(defn solve-puzzle (scan-result) ...) ; Returns a valueMission author verifies:
(:acceptance (lambda (script-output scan-result) ; Predicate: does output satisfy the mission? (and (list? script-output) (every node? script-output) (every (lambda (n) (> (threat-level n) 2)) script-output))))State Isolation
Section titled “State Isolation”- Memory: Script runs in a dedicated 4–8 KB arena. Freed on mission completion.
- Bindings: Variables in the script are local. No global state pollution.
- Undo: If script fails, no side effects persist. Player can re-run from the same input.
Acceptance Contract Format
Section titled “Acceptance Contract Format”The mission template declares how the player’s script output is verified.
Structure
Section titled “Structure”(defmission "FILTER HOSTILE NODES" (:doc "Write a function that selects nodes with threat level > 2") (:threat-range 1 2)
(:input-template (generate-scan-result threat-level))
(:expected-script ; Type hint for player (shown in editor) (lambda (scan-result) (list-of nodes)))
(:acceptance-contract (lambda (script-output input-data) ; Predicate: does the output satisfy mission goals? ; Returns (pass-or-fail clause-results) (let* ((nodes (nodes-from-scan input-data)) (selected script-output) (correct (filter (lambda (n) (> (threat-level n) 2)) nodes)) (matches-exactly (equal? selected correct)) (no-extras (every (lambda (n) (member? n correct)) selected)) (no-missing (every (lambda (n) (member? n selected)) correct))) (if (and matches-exactly no-extras no-missing) (pass) (fail (:no-extras? no-extras "Your result includes non-hostile nodes") (:no-missing? no-missing "You missed some hostile nodes") (:exact-match? matches-exactly "Your selection doesn't match expected"))))))
(:difficulty 1) (:hints-available? true) (:hint-1 "Use filter with a predicate that checks threat-level"))Components
Section titled “Components”| Component | Type | Purpose |
|---|---|---|
:input-template | fn | Generates mission input (can be procedural based on threat) |
:expected-script | type hint | Shown in editor; guides player (not enforced) |
:acceptance-contract | predicate fn | Evaluates script output; returns detailed pass/fail |
:difficulty | int 1–5 | Pacing (1 = tutorial, 5 = expert) |
:hints-available? | bool | If true, player can request hints |
:hint-N | string | Incremental hints (don’t spoil answer) |
Pass/Fail Return
Section titled “Pass/Fail Return”(pass); => Returns to mission complete; credits awarded; rep +
(fail (:clause-1 false "Explanation of what went wrong") (:clause-2 true "This part was correct") (:clause-3 false "Your output is incomplete")); => Shows to player:; ✓ clause-2; ✗ clause-1 (Explanation of what went wrong); ✗ clause-3 (Your output is incomplete); (try again / request hint / abort)Worked Example 1: Beginner Mission (One-Liner)
Section titled “Worked Example 1: Beginner Mission (One-Liner)”Title: “SELECT HOSTILE NODES”
Narrative: “You’ve scanned a network segment. A list of nodes came back. Filter out the safe ones—keep only nodes with threat level > 2.”
Mission template:
(defmission "SELECT HOSTILE NODES" (:doc "Write a function that filters nodes by threat level") (:threat-range 1 2) (:difficulty 1)
(:input-template (lambda () (let ((lfsr (lfsr-init 0xDEAD))) (list (make-node :id 1 :threat 1 :compromised false) (make-node :id 2 :threat 3 :compromised false) (make-node :id 3 :threat 2 :compromised false) (make-node :id 4 :threat 4 :compromised false)))))
(:expected-script (lambda (nodes) ; Your script should return a filtered list ))
(:acceptance-contract (lambda (player-result input) (let* ((nodes input) (correct (filter (lambda (n) (> (threat n) 2)) nodes)) (match (equal? player-result correct))) (if match (pass) (fail (:correct-filter match "Your result should include only nodes with threat > 2"))))))
(:hints-available? true) (:hint-1 "Use (filter ...) to select from a list") (:hint-2 "Your predicate should test (> (threat node) 2)") (:hint-3 "Example: (filter your-nodes (lambda (n) (> (threat n) 2)))")
(:reward-credits 100) (:reward-reputation 1))Player’s expected solution:
(lambda (nodes) (filter nodes (lambda (n) (> (threat n) 2))))Evaluation:
- Mission framework passes
nodes(the input from:input-template). - Player script runs. Output:
(node-2 node-4)(the two with threat > 2). - Acceptance contract evaluates: correct =
(filter nodes ...)also returns(node-2 node-4). (equal? player-result correct)→ true.- Mission success. 100 ¤, rep +1.
Worked Example 2: Advanced Mission (Multi-Cartridge)
Section titled “Worked Example 2: Advanced Mission (Multi-Cartridge)”Title: “AUDIT THE INTRUSION TRAIL”
Narrative: “You’ve breached a financial network and extracted transaction records. BLACK LEDGER is available. Write a function that uses data from both ICE Breaker and BLACK LEDGER to identify which transactions funded the intrusion.”
Context: This mission requires ICE Breaker and BLACK LEDGER both loaded. It orchestrates across cartridges.
Mission template:
(defmission "AUDIT THE INTRUSION TRAIL" (:doc "Use ICE Breaker node data + BLACK LEDGER transactions to trace fund flow") (:requires ice-breaker black-ledger) (:threat-range 3 4) (:difficulty 4)
(:input-template (lambda () (let ((lfsr (lfsr-init 0xCAFE))) (list :ice-breaker-nodes (list (make-node :id 1 :threat 4 :data-handle 0x42) (make-node :id 2 :threat 2 :data-handle 0x43) (make-node :id 3 :threat 3 :data-handle 0x44)) :ledger-transactions (list (make-tx :from "ATTACKER" :to "NODE_1" :amount 5000) (make-tx :from "NODE_2" :to "ACCOUNT_X" :amount 2000) (make-tx :from "ACCOUNT_Y" :to "NODE_3" :amount 8000))))))
(:expected-script (lambda (ice-nodes ledger-txs) ; Return list of (node-id . transaction) pairs ; showing which transactions funded each compromised node ))
(:grants (list :cartridge-data :ice-breaker :cartridge-data :black-ledger))
(:acceptance-contract (lambda (player-result input) (let* ((nodes (getf input :ice-breaker-nodes)) (txs (getf input :ledger-transactions)) ; Correct answer: match nodes by data-handle to transactions (correct (map (lambda (node) (let ((handle (data-handle node)) (matching-txs (filter txs (lambda (tx) (eq? (tx-to tx) (number->string handle)))))) (cons (node-id node) matching-txs))) nodes)) ; Verify structure (structure-ok (every pair? player-result)) ; Verify content (content-matches (equal? player-result correct))) (if (and structure-ok content-matches) (pass) (fail (:structure structure-ok "Result should be list of (node-id . transactions) pairs") (:content content-matches "Transaction matching doesn't align with intrusion nodes"))))))
(:hints-available? true) (:hint-1 "You need to correlate node data-handles with transaction targets") (:hint-2 "Use map to transform each node into (id . related-txs)") (:hint-3 "Use filter on txs to find those matching each node's handle")
(:reward-credits 400) (:reward-reputation 2))Player’s expected solution:
(lambda (ice-nodes ledger-txs) (map (lambda (node) (cons (node-id node) (filter ledger-txs (lambda (tx) (= (tx-to tx) (number->string (data-handle node))))))) ice-nodes))Evaluation:
- Mission provides both ICE nodes and ledger transactions.
- Player script maps over nodes, filtering transactions by data-handle correlation.
- Output is a list of pairs:
((1 . (tx1)) (2 . ()) (3 . (tx3))). - Acceptance contract verifies structure (all pairs) and content (correct transactions).
- If both pass: mission success. 400 ¤, rep +2.
Why advanced:
- Requires understanding two data structures.
- Combines map + filter (higher-order thinking).
- Necessitates a cross-cartridge join operation.
- Difficulty 4 (expert players).
Mission Difficulty Pacing
Section titled “Mission Difficulty Pacing”Progression on campaign path:
| Phase | Example | Difficulty | Script Type | Hints |
|---|---|---|---|---|
| Tutorial | ”Return the first element” | 1 | (car list) | Extensive |
| Early game | ”Filter by predicate” | 1–2 | (filter list pred) | Moderate |
| Mid game | ”Transform and filter” | 2–3 | (map transform) + (filter pred) | Moderate |
| Late game | ”Multi-cartridge join” | 3–4 | (map + filter + cross-cartridge) | Minimal |
| Post-game | ”Write custom DSL” | 4–5 | Advanced meta-programming | Minimal |
Design principle: Early missions are one-liners or simple two-liners. Late missions require composition of 3+ functions.
Error Reporting
Section titled “Error Reporting”When a script fails verification, the player sees clause-level feedback, not a generic “wrong answer.”
Example failure (beginner mission):
Script submitted.
Evaluating acceptance contract...
✗ THREAT FILTER Your result includes non-hostile nodes (threat ≤ 2). Expected only nodes with threat > 2.
Try again? / Request hint / Abort missionExample failure (advanced mission):
Script submitted.
Evaluating acceptance contract...
✓ Result structure (is list of pairs)✗ Node 2 mapping You linked node 2 to transaction TX_B. But node 2's data-handle doesn't match TX_B's target.✗ Node 3 mapping Missing link to transaction TX_C. Check if your filter is finding all matches.
Try again? / Request hint / Abort missionBenefit: Player gets diagnostic clues without the solution spoiled.
Safe Termination Guarantees
Section titled “Safe Termination Guarantees”Problem: Player’s script might infinite-loop or consume memory.
Solution: Execution boundaries.
(execute-with-timeout script-expression :timeout-ms 1000 ; 1 second max :memory-limit-bytes 8192 ; 8 KB arena :on-timeout (fail (:timeout true "Script took too long. Infinite loop?")) :on-oom (fail (:oom true "Script used too much memory")))Mechanism:
- The VM interpreter counts bytecode instructions.
- After N instructions (timeout-ms worth), raise exception.
- If arena exceeds limit, allocation fails; GC (if any) can’t recover; mission fails.
Reasonable limits:
- Timeout: 1 second (plenty for typical filter/map operations).
- Memory: 8 KB (enough for intermediate results, not full dataset).
Open Questions
Section titled “Open Questions”1. Can Scripts Spawn Cells?
Section titled “1. Can Scripts Spawn Cells?”Question: Should mission scripts be able to create cell objects (network-node, transaction, etc.) for advanced tests?
Current answer: No. Cells are cartridge-only. Scripts work with data structures, not cells.
Rationale: Cells have identity and persistence; giving scripts cell-spawning power breaks encapsulation.
Future: v1.1 could allow “synthetic cell” creation (ephemeral, read-only copies of real cells) for advanced missions.
2. Cross-Cartridge Data Leakage?
Section titled “2. Cross-Cartridge Data Leakage?”Question: If a mission grants access to ICE Breaker and BLACK LEDGER simultaneously, what prevents a script from reading internals?
Current answer: :grants is explicit. Mission author lists exactly which cartridge data is available. Everything else is forbidden.
Enforcement: The VM’s FFI layer checks :grants before returning any cartridge-specific data.
3. Can Players Save & Reuse Snippets?
Section titled “3. Can Players Save & Reuse Snippets?”Question: Should players be able to save a solution function for use in future missions?
Answer (post-v1): Yes. A per-deck script library (stored in deck state, portable via link cable) allows this. v1 doesn’t require it; nice-to-have.
4. What if Script Aborts Mid-Execution?
Section titled “4. What if Script Aborts Mid-Execution?”Question: Can a script call (fail ...) explicitly (not just via acceptance contract)?
Current answer: Yes. Scripts can abandon themselves if they detect an invalid state (defensive coding).
Example:
(if (null? input) (fail (:invalid-input true "Input is empty")) ; proceed...)Implementation Summary
Section titled “Implementation Summary”| Aspect | Specification |
|---|---|
| FFI Access | Tier 1 (always), Tier 2 (granted), Tier 3 (forbidden) |
| Input contract | :input-template generates data |
| Output contract | :acceptance-contract predicate verifies result |
| Isolation | 4–8 KB arena, 1s timeout, memory limit |
| Error reporting | Clause-scoped feedback, not spoiled |
| Difficulty pacing | 1 (tutorial) to 5 (expert) |
| Examples | 2 worked examples (beginner, advanced) |
| Post-v1 | Snippet library, learned hints, synthetic cells |
Recommended Next Steps
Section titled “Recommended Next Steps”-
Enumerate Tier 1 FFI surface: Exact signatures for all always-available functions. Reference
nosh_runtime.candnosh_stdlib.c. Estimate 1 day. -
Implement acceptance-contract evaluator: VM support for running predicates and clause introspection. Estimate 2 days.
-
Write mission author guide: Template format, example missions, testing checklist. Estimate 1 day.
-
Playtest beginner missions: Recruit 3–5 testers. Iterate on difficulty curve. Estimate 2 days.
-
Design post-launch snippet library: Schema, storage, sharing mechanics. Estimate 1 day.
Lisp-scripted missions enable creative problem-solving. The acceptance contract model lets mission authors specify puzzles without giving away solutions. Tier-based FFI access keeps scripts safe while preserving gameplay challenge.