Skip to content

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.


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:

  1. Single-cartridge: “Write a filter function that selects nodes by threat level.”
  2. Multi-capability: “Write a function that chains ICE Breaker reconnaissance with BLACK LEDGER transaction tracing.”
  3. Free-play REPL: Player writes and executes code in the nOSh runtime REPL for learning.

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 construction

String/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 introspection

Tier 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)

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 job

No access to other cartridges’ internal state:

(black-ledger-transaction-list) ; FORBIDDEN — inter-cartridge leakage
(neongrid-current-position) ; FORBIDDEN

No reflection/metaprogramming:

(eval ...) ; FORBIDDEN — dynamic code eval
(load-file ...) ; FORBIDDEN
(intern symbol) ; FORBIDDEN

Each scripted mission runs in an isolated arena with its own state.

Provided by mission template:

(:input (() . input-value)) ; A Lisp value passed to the script

Example: ICE Breaker extraction mission provides:

(mission-input)
; => (struct scan-result
; (nodes (list node-1 node-2 node-3))
; (threat-level 3)
; (time-limit 8))

Player script returns:

(defn solve-puzzle (scan-result)
...) ; Returns a value

Mission 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))))
  • 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.

The mission template declares how the player’s script output is verified.

(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"))
ComponentTypePurpose
:input-templatefnGenerates mission input (can be procedural based on threat)
:expected-scripttype hintShown in editor; guides player (not enforced)
:acceptance-contractpredicate fnEvaluates script output; returns detailed pass/fail
:difficultyint 1–5Pacing (1 = tutorial, 5 = expert)
:hints-available?boolIf true, player can request hints
:hint-NstringIncremental hints (don’t spoil answer)
(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:

  1. Mission framework passes nodes (the input from :input-template).
  2. Player script runs. Output: (node-2 node-4) (the two with threat > 2).
  3. Acceptance contract evaluates: correct = (filter nodes ...) also returns (node-2 node-4).
  4. (equal? player-result correct) → true.
  5. 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:

  1. Mission provides both ICE nodes and ledger transactions.
  2. Player script maps over nodes, filtering transactions by data-handle correlation.
  3. Output is a list of pairs: ((1 . (tx1)) (2 . ()) (3 . (tx3))).
  4. Acceptance contract verifies structure (all pairs) and content (correct transactions).
  5. 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).

Progression on campaign path:

PhaseExampleDifficultyScript TypeHints
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–5Advanced meta-programmingMinimal

Design principle: Early missions are one-liners or simple two-liners. Late missions require composition of 3+ functions.


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 mission

Example 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 mission

Benefit: Player gets diagnostic clues without the solution spoiled.


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).

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.

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.

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.

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...
)

AspectSpecification
FFI AccessTier 1 (always), Tier 2 (granted), Tier 3 (forbidden)
Input contract:input-template generates data
Output contract:acceptance-contract predicate verifies result
Isolation4–8 KB arena, 1s timeout, memory limit
Error reportingClause-scoped feedback, not spoiled
Difficulty pacing1 (tutorial) to 5 (expert)
Examples2 worked examples (beginner, advanced)
Post-v1Snippet library, learned hints, synthetic cells

  1. Enumerate Tier 1 FFI surface: Exact signatures for all always-available functions. Reference nosh_runtime.c and nosh_stdlib.c. Estimate 1 day.

  2. Implement acceptance-contract evaluator: VM support for running predicates and clause introspection. Estimate 2 days.

  3. Write mission author guide: Template format, example missions, testing checklist. Estimate 1 day.

  4. Playtest beginner missions: Recruit 3–5 testers. Iterate on difficulty curve. Estimate 2 days.

  5. 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.