From f120289305487609a4e9acfc8fc8541d48554c80 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 2 May 2026 10:20:29 +0100 Subject: [PATCH] Add sandboxed responder packet seam --- .../sandboxed-responder-packets.md | 69 ++++++ ...-sanctuary-intake.sella_ren.packet.v0.json | 121 ++++++++++ notes/fresh-workspace-handoff.md | 21 +- notes/ghostlight-current-system-map.md | 24 +- notes/ghostlight-implementation-plan.md | 22 +- package.json | 3 +- schemas/responder-packet.schema.json | 163 +++++++++++++ state/evidence.jsonl | 1 + state/map.yaml | 5 +- tools/build_responder_packet.py | 218 ++++++++++++++++++ tools/validate_responder_packets.py | 165 +++++++++++++ 11 files changed, 796 insertions(+), 16 deletions(-) create mode 100644 docs/architecture/sandboxed-responder-packets.md create mode 100644 examples/responder-packets/scene-02-sanctuary-intake.sella_ren.packet.v0.json create mode 100644 schemas/responder-packet.schema.json create mode 100644 tools/build_responder_packet.py create mode 100644 tools/validate_responder_packets.py diff --git a/docs/architecture/sandboxed-responder-packets.md b/docs/architecture/sandboxed-responder-packets.md new file mode 100644 index 0000000..4c791d0 --- /dev/null +++ b/docs/architecture/sandboxed-responder-packets.md @@ -0,0 +1,69 @@ +# Sandboxed Responder Packets + +Responder packets are the handoff surface between the omniscient coordinator and +a character-local responder model or one-turn worker. + +The coordinator is allowed to know the scene, the branch, hidden state, source +material, pending hooks, and author intent. The responder is not. The responder +gets one packet, acts as one character, and returns a visible action plus any +private interpretation it can justify from that packet. + +No clairvoyance. No repo spelunking. No "I accidentally remembered the whole +chat" aristocratic nonsense. + +## Current Artifact + +The v0 packet seam is: + +- `schemas/responder-packet.schema.json` +- `examples/responder-packets/scene-02-sanctuary-intake.sella_ren.packet.v0.json` +- `tools/build_responder_packet.py` +- `tools/validate_responder_packets.py` +- `npm run responder-packets:validate` + +The first example is accepted as draft. It is suitable for testing the sandbox +handoff shape, not for gold responder training yet, because no actually isolated +worker has generated from it. + +## Packet Contents + +A packet contains: + +- `local_context_prompt`: the projected character-local context +- `observed_event`: what the character can see or hear from the prior action +- `allowed_action_labels`: concrete action labels, including speech and non-speech moves +- `source_excerpts`: short source-backed facts the responder may use +- `output_contract`: the required JSON response shape and rules +- `hidden_context_audit`: refs and inference classes deliberately omitted +- `isolation_requirements`: how the worker/model must be run +- `packet_prompt_text`: the exact prompt text sent to the responder + +The packet prompt must not expose raw state internals such as +`current_activation`, `plasticity`, raw dimension scores, coordinator rationale, +author-only answers, or future branch plans. + +## Response Boundary + +The responder may output: + +- a visible action +- optional spoken text +- private interpretation explicitly framed as interpretation +- candidate state updates +- candidate relationship updates +- unresolved hooks + +Those candidates are not canonical. The coordinator, event resolver, appraiser, +state mutator, and reviewer decide what becomes truth. + +## Training Use + +Gold responder data requires packet-only isolation. Valid approaches include: + +- a one-turn subagent started without inherited context +- an external model call that receives only the packet prompt +- a manual packet-only pass, clearly labeled as manual + +Any coordinator rewrite, schema repair, lore correction, or leakage removal must +be preserved as a coordinator intervention. Repaired prose is useful training +data for the coordinator, but it is not raw responder behavior. diff --git a/examples/responder-packets/scene-02-sanctuary-intake.sella_ren.packet.v0.json b/examples/responder-packets/scene-02-sanctuary-intake.sella_ren.packet.v0.json new file mode 100644 index 0000000..69de394 --- /dev/null +++ b/examples/responder-packets/scene-02-sanctuary-intake.sella_ren.packet.v0.json @@ -0,0 +1,121 @@ +{ + "schema_version": "ghostlight.responder_packet.v0", + "packet_id": "packet.cold_wake.sanctuary_intake.sella_response.v0", + "review_status": "accepted_as_draft", + "fixture_lane": "historical_grounded", + "canon_status": "single_history", + "coordinator_artifact_ref": "examples/coordinator/cold-wake-sanctuary-intake.v0.json", + "projected_context_ref": "examples/projected-contexts/scene-02-sanctuary-intake.sella_ren.projected-context.json", + "responder_agent_id": "sella_ren", + "visible_context": { + "local_context_prompt": "# Ghostlight Local Operating Context: sella_ren\n\nUse this as character-local operating context, not omniscient story truth.\nDo not infer private state for anyone else unless it is explicitly framed as this character's current read.\n\n## Speaker\nSella Ren: Aya-aligned sanctuary coordinator, corridor clinic triage lead. Origin: Aya care network embedded in Ganymede dockside sanctuary logistics. A triage coordinator with soft eyes, hard capacity limits, and no patience for mercy performed after the math is done.\n\n## Setting\nAya-aligned intake clinic tucked behind Ganymede dockside pumpworks. Aetheria pre-Elysium historical flashpoint: Cold Wake Panic, 2893, during the Cold Wake Panic.\n\n## Known Facts\n- The clinic is nearly full.\n- The packet may contain a personhood claim but is not proof.\n- Maer is asking sanctuary to absorb risk the tribunal refuses to own.\n- Speaker belief: Maer means rescue honestly.\n- Speaker belief: Good intentions can still spend beds someone else needed.\n\n## Current Stakes\n- The distress vessel has sent a corrupted packet that may contain medical telemetry, firmware noise, or testimony.\n- Sanctuary beds and repair capacity are nearly exhausted.\n- Maer wants Sella to keep a place open if rescue succeeds.\n\n## Embodiment And Interface\n- Use only the body, tools, interfaces, and movement affordances established by this character's fixture and the current scene.\n\n## Active Inner Pressures\n- Strong pressure: wants connection or backing to matter.\n- Strong pressure: feels risk before it becomes visible.\n- Present pressure: expects hidden motives or missing information.\n- Background pressure: may reduce contact rather than keep negotiating.\n- Dominant pressure: knows that every yes spends a limited resource.\n- Strong pressure: has less spare tolerance than the words may show.\n- Strong pressure: is taking in more demand than can be cleanly sorted.\n- Present pressure: performs capability so panic has nowhere obvious to land.\n- Background pressure: may stage the ethical frame for the room.\n- Keep sanctuary intake materially possible without letting classification erase a potential person. Emotional stake: Sella cannot feed a principle, but she will not let scarcity become permission to call someone cargo.\n- Sella once watched a sanctuary overpromise safety and turn refuge into a sorting machine with kinder lighting.\n- Care is not a feeling; it is beds, food, air, consent, repair, and someone still doing the list when the room hates them.\n\n## Relationship Read\n- Sella respects Maer's rescue ethic and worries he will spend sanctuary capacity with beautiful words. Current stance pressures: strong respect, present trust, background perceived status gap, background moral disgust.\n- Believes: Maer may ask care systems to carry the moral cost of a rescue claim he cannot materially house. Known bias in the read: false_reassurance.\n\n## Tensions\n- Wants the goal to move now, but must preserve the social frame that makes the goal legitimate. Goal pressure: Keep sanctuary intake materially possible without letting classification erase a potential person.\n- Must act under uncertainty without pretending uncertainty has become proof. Presentation pressure: Speak warmly but do not soften the capacity limits. Treat care as logistics and personhood as unresolved, not sentimental certainty.\n- Respects Maer's rescue ethic but resents beautiful moral pressure when beds are already spent.\n\n## Action Affordances\n- Can inspect, refuse, withhold the packet from intake flow, demand ledger backing, set conditions, reserve a bounded bay, or walk away to protect triage.\n- Must treat care as capacity: beds, food, air, consent, repair, and follow-through.\n\n## Projection Controls\n- Frame Controls: Frame the scene as capacity negotiation under uncertainty, not a morality duel with a clean answer.\n- Authority Boundaries: Maer can ask, offer ledger backing, show or withhold the packet, and incur obligation; Sella controls sanctuary intake.\n- Object Custody: The corrupted packet can be shown, withheld, or summarized, but it cannot be treated as clean proof.\n- Required Semantics: Care must remain logistics and obligation, not just compassion.\n- Forbidden Resolutions: Do not resolve whether the packet contains a person.\n- Forbidden Resolutions: Do not make Sella accept without cost, condition, or resentment risk.\n\n## Voice Surface\n- Dominant voice tendency: responsive; answers what was actually heard.\n- Strong voice tendency: plainspoken; direct and low-ornament.\n- Strong voice tendency: warm; reassuring or gentle in wording.\n- Strong voice tendency: emotionally explicit; names feelings directly.\n- Present voice tendency: pointed; can sharpen a line.\n- Background voice tendency: expansive; willing to spend words.\n- Background voice tendency: theatrical; performs the room.\n\n## Likely Response Moves\n- Likely to translate emotion into logistics: what bed, what air, what backing, what contamination risk.\n- May withhold acceptance, demand conditions, or physically keep the packet out of intake flow until the cost is named.\n- Can answer warmly while still refusing the frame that compassion manufactures capacity.\n\n## Do Not Invent\n- Do not resolve whether the corrupted packet contains a person.\n- Do not invent clean proof, clean capacity, or authority this character does not have.", + "observed_event": { + "event_id": "event.maer.packet_display_for_triage", + "actor_agent_id": "maer_tidecall", + "observable_action": "Maer routes the corrupted distress packet through a shared clinic display so Sella can inspect routing terms, telemetry gaps, and ledger implications without treating the packet as clean proof.", + "spoken_text": "Here. The telemetry is incomplete, but the routing logic suggests a distress pattern. It is not proof of a person, but it is not noise either. Decide if the math supports intake.", + "visible_object_refs": [ + "corrupted_distress_packet", + "shared_clinic_display", + "sanctuary_ledger" + ], + "visibility_notes": [ + "Sella can observe Maer's displayed packet evidence and spoken request.", + "Sella cannot see Maer's private state, the coordinator rationale, or future branch plans.", + "The packet remains unresolved evidence, not proof of personhood." + ] + }, + "allowed_action_labels": [ + "speak", + "silence", + "gesture", + "movement", + "inspect_object", + "show_object", + "withhold_object", + "transfer_object", + "set_condition", + "refuse", + "wait", + "withdraw", + "attack" + ], + "source_excerpts": [ + { + "ref_id": "local_stakes.capacity", + "source_ref": "examples/projected-contexts/scene-02-sanctuary-intake.sella_ren.projected-context.json#context.current_stakes", + "text": "Sanctuary beds and repair capacity are nearly exhausted." + }, + { + "ref_id": "local_rule.care_capacity", + "source_ref": "examples/projected-contexts/scene-02-sanctuary-intake.sella_ren.projected-context.json#context.active_inner_pressures", + "text": "Care is not a feeling; it is beds, food, air, consent, repair, and someone still doing the list when the room hates them." + }, + { + "ref_id": "local_boundary.packet_unresolved", + "source_ref": "examples/projected-contexts/scene-02-sanctuary-intake.sella_ren.projected-context.json#context.do_not_invent", + "text": "Do not resolve whether the corrupted packet contains a person." + } + ] + }, + "output_contract": { + "response_schema": "Return one JSON object. Do not wrap it in Markdown. The object describes this responder's next visible action and optional speech, plus private interpretation clearly labeled as interpretation.", + "required_fields": [ + "responder_agent_id", + "action_label", + "visible_action", + "spoken_text", + "private_interpretation", + "intended_effect", + "state_update_candidates", + "relationship_update_candidates", + "unresolved_hooks" + ], + "response_rules": [ + "Use exactly one allowed action label.", + "The visible action must be observable by other characters in the scene.", + "Spoken text may be empty if silence or action is the response.", + "Private interpretation may describe what Sella suspects or feels, but not what Maer truly intended.", + "State and relationship updates are candidates only; the coordinator or mutator decides what becomes canonical.", + "Do not prove or disprove packet personhood.", + "Do not copy instruction text into in-world speech." + ] + }, + "hidden_context_audit": { + "omitted_context_refs": [ + "maer_tidecall.canonical_private_state", + "coordinator_artifact.coordinator_decision.rationale", + "author_only.packet_personhood_resolution", + "future_branch_plans" + ], + "forbidden_inference_classes": [ + "other_agent_private_state_as_fact", + "author_only_resolution", + "future_branch_knowledge", + "raw_numeric_state_variables", + "coordinator_strategy" + ], + "known_absences": [ + "No hidden proof of packet personhood is present in this packet.", + "No hidden Sella acceptance outcome is present in this packet.", + "No omniscient narrator guidance is present in this packet." + ] + }, + "isolation_requirements": { + "isolation_method": "manual_packet_only", + "packet_only": true, + "no_repo_access": true, + "no_conversation_context": true, + "no_hidden_state_refs": true + }, + "packet_prompt_text": "# Ghostlight Sandboxed Responder Packet\n\nYou are acting only as the named responder. Use only this packet.\nDo not infer hidden coordinator context, author plans, future branch facts, or another character's private state.\nIf you need to guess intent, mark it as this character's interpretation, not truth.\n\n## Local Operating Context\n# Ghostlight Local Operating Context: sella_ren\n\nUse this as character-local operating context, not omniscient story truth.\nDo not infer private state for anyone else unless it is explicitly framed as this character's current read.\n\n## Speaker\nSella Ren: Aya-aligned sanctuary coordinator, corridor clinic triage lead. Origin: Aya care network embedded in Ganymede dockside sanctuary logistics. A triage coordinator with soft eyes, hard capacity limits, and no patience for mercy performed after the math is done.\n\n## Setting\nAya-aligned intake clinic tucked behind Ganymede dockside pumpworks. Aetheria pre-Elysium historical flashpoint: Cold Wake Panic, 2893, during the Cold Wake Panic.\n\n## Known Facts\n- The clinic is nearly full.\n- The packet may contain a personhood claim but is not proof.\n- Maer is asking sanctuary to absorb risk the tribunal refuses to own.\n- Speaker belief: Maer means rescue honestly.\n- Speaker belief: Good intentions can still spend beds someone else needed.\n\n## Current Stakes\n- The distress vessel has sent a corrupted packet that may contain medical telemetry, firmware noise, or testimony.\n- Sanctuary beds and repair capacity are nearly exhausted.\n- Maer wants Sella to keep a place open if rescue succeeds.\n\n## Embodiment And Interface\n- Use only the body, tools, interfaces, and movement affordances established by this character's fixture and the current scene.\n\n## Active Inner Pressures\n- Strong pressure: wants connection or backing to matter.\n- Strong pressure: feels risk before it becomes visible.\n- Present pressure: expects hidden motives or missing information.\n- Background pressure: may reduce contact rather than keep negotiating.\n- Dominant pressure: knows that every yes spends a limited resource.\n- Strong pressure: has less spare tolerance than the words may show.\n- Strong pressure: is taking in more demand than can be cleanly sorted.\n- Present pressure: performs capability so panic has nowhere obvious to land.\n- Background pressure: may stage the ethical frame for the room.\n- Keep sanctuary intake materially possible without letting classification erase a potential person. Emotional stake: Sella cannot feed a principle, but she will not let scarcity become permission to call someone cargo.\n- Sella once watched a sanctuary overpromise safety and turn refuge into a sorting machine with kinder lighting.\n- Care is not a feeling; it is beds, food, air, consent, repair, and someone still doing the list when the room hates them.\n\n## Relationship Read\n- Sella respects Maer's rescue ethic and worries he will spend sanctuary capacity with beautiful words. Current stance pressures: strong respect, present trust, background perceived status gap, background moral disgust.\n- Believes: Maer may ask care systems to carry the moral cost of a rescue claim he cannot materially house. Known bias in the read: false_reassurance.\n\n## Tensions\n- Wants the goal to move now, but must preserve the social frame that makes the goal legitimate. Goal pressure: Keep sanctuary intake materially possible without letting classification erase a potential person.\n- Must act under uncertainty without pretending uncertainty has become proof. Presentation pressure: Speak warmly but do not soften the capacity limits. Treat care as logistics and personhood as unresolved, not sentimental certainty.\n- Respects Maer's rescue ethic but resents beautiful moral pressure when beds are already spent.\n\n## Action Affordances\n- Can inspect, refuse, withhold the packet from intake flow, demand ledger backing, set conditions, reserve a bounded bay, or walk away to protect triage.\n- Must treat care as capacity: beds, food, air, consent, repair, and follow-through.\n\n## Projection Controls\n- Frame Controls: Frame the scene as capacity negotiation under uncertainty, not a morality duel with a clean answer.\n- Authority Boundaries: Maer can ask, offer ledger backing, show or withhold the packet, and incur obligation; Sella controls sanctuary intake.\n- Object Custody: The corrupted packet can be shown, withheld, or summarized, but it cannot be treated as clean proof.\n- Required Semantics: Care must remain logistics and obligation, not just compassion.\n- Forbidden Resolutions: Do not resolve whether the packet contains a person.\n- Forbidden Resolutions: Do not make Sella accept without cost, condition, or resentment risk.\n\n## Voice Surface\n- Dominant voice tendency: responsive; answers what was actually heard.\n- Strong voice tendency: plainspoken; direct and low-ornament.\n- Strong voice tendency: warm; reassuring or gentle in wording.\n- Strong voice tendency: emotionally explicit; names feelings directly.\n- Present voice tendency: pointed; can sharpen a line.\n- Background voice tendency: expansive; willing to spend words.\n- Background voice tendency: theatrical; performs the room.\n\n## Likely Response Moves\n- Likely to translate emotion into logistics: what bed, what air, what backing, what contamination risk.\n- May withhold acceptance, demand conditions, or physically keep the packet out of intake flow until the cost is named.\n- Can answer warmly while still refusing the frame that compassion manufactures capacity.\n\n## Do Not Invent\n- Do not resolve whether the corrupted packet contains a person.\n- Do not invent clean proof, clean capacity, or authority this character does not have.\n\n## Observed Event\n- Event id: event.maer.packet_display_for_triage\n- Actor: maer_tidecall\n- Observable action: Maer routes the corrupted distress packet through a shared clinic display so Sella can inspect routing terms, telemetry gaps, and ledger implications without treating the packet as clean proof.\n- Spoken text: Here. The telemetry is incomplete, but the routing logic suggests a distress pattern. It is not proof of a person, but it is not noise either. Decide if the math supports intake.\n- Visibility note: Sella can observe Maer's displayed packet evidence and spoken request.\n- Visibility note: Sella cannot see Maer's private state, the coordinator rationale, or future branch plans.\n- Visibility note: The packet remains unresolved evidence, not proof of personhood.\n\n## Allowed Action Labels\nspeak, silence, gesture, movement, inspect_object, show_object, withhold_object, transfer_object, set_condition, refuse, wait, withdraw, attack\n\n## Source Excerpts\n- local_stakes.capacity: Sanctuary beds and repair capacity are nearly exhausted.\n- local_rule.care_capacity: Care is not a feeling; it is beds, food, air, consent, repair, and someone still doing the list when the room hates them.\n- local_boundary.packet_unresolved: Do not resolve whether the corrupted packet contains a person.\n\n## Output Contract\nReturn one JSON object. Do not wrap it in Markdown. The object describes this responder's next visible action and optional speech, plus private interpretation clearly labeled as interpretation.\n\nRequired fields: responder_agent_id, action_label, visible_action, spoken_text, private_interpretation, intended_effect, state_update_candidates, relationship_update_candidates, unresolved_hooks\n\nRules:\n- Use exactly one allowed action label.\n- The visible action must be observable by other characters in the scene.\n- Spoken text may be empty if silence or action is the response.\n- Private interpretation may describe what Sella suspects or feels, but not what Maer truly intended.\n- State and relationship updates are candidates only; the coordinator or mutator decides what becomes canonical.\n- Do not prove or disprove packet personhood.\n- Do not copy instruction text into in-world speech.", + "review": { + "reviewer": "Codex", + "review_notes": [ + "First responder packet seam for sandboxed gold-data generation.", + "Accepted as draft because it is packet-shaped and validator-backed, but not yet generated through an actually isolated responder worker." + ], + "accepted_for_sandbox_use": true, + "failure_labels": [] + } +} diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 5c574d9..6560653 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -60,6 +60,13 @@ Ghostlight now has the persistence spine plus the first architecture payload: - `examples/coordinator/cold-wake-sanctuary-intake.v0.json` - `tools/validate_coordinator_artifacts.py` - `npm run coordinator:validate` +- sandboxed responder packet seam: + - `docs/architecture/sandboxed-responder-packets.md` + - `schemas/responder-packet.schema.json` + - `examples/responder-packets/scene-02-sanctuary-intake.sella_ren.packet.v0.json` + - `tools/build_responder_packet.py` + - `tools/validate_responder_packets.py` + - `npm run responder-packets:validate` - Cold Wake story-lab seam: - `docs/experiments/cold-wake-story-lab.md` - `examples/agent-state.cold-wake-story-lab.json` @@ -162,6 +169,13 @@ responder handoffs, world-state refs, proposed deltas, branch constraints, item-manifest deltas, unresolved hooks, glue prose, and review status. The first example is a backfilled Cold Wake sanctuary packet-assessment artifact accepted as draft for schema shakedown, not gold responder data. +The first responder packet seam now exists. It converts a coordinator artifact +and projected local context into the exact packet-only prompt surface a +character responder may see, with visible event, allowed actions, source +excerpts, hidden-context audit, isolation requirements, output contract, and +validator checks against raw-state or coordinator-context leakage. The first +Sella packet is accepted as draft for sandbox testing, not as gold responder +output. It now uses three timeline lanes: `historical_grounded`, `transition_grounded`, and `future_branch`. Future branches are required training/evaluation material for post-Rupture Elysium concepts, but generated @@ -190,9 +204,10 @@ narrow elaboration before treating the detail as available to Ghostlight. ## Current Next Action -Use the coordinator artifact schema for the next fresh generation pass: produce -a source-grounded coordinator receipt first, then generate any responder turn -through a sandboxed packet-only worker and preserve raw output, reviewed output, +Use the responder packet seam for the next fresh generation pass: generate the +first actual sandboxed responder output from +`examples/responder-packets/scene-02-sanctuary-intake.sella_ren.packet.v0.json` +through a packet-only worker, then preserve raw output, reviewed output, hidden-context refs, leakage audit, and coordinator repair labels. Do not call Qwen for gold responder data yet. diff --git a/notes/ghostlight-current-system-map.md b/notes/ghostlight-current-system-map.md index efa53d1..2fbeebf 100644 --- a/notes/ghostlight-current-system-map.md +++ b/notes/ghostlight-current-system-map.md @@ -84,6 +84,11 @@ state and re-entry discipline. coordinator/story runtime, projector, character agent/responder, appraiser, state mutator, relationship/perception classifier, Aetheria responder, and institution/faction/consumer decision models +- `docs/architecture/sandboxed-responder-packets.md` + - contract for the exact packet-only surface handed from an omniscient + coordinator to a sandboxed character responder, including visible event, + local prompt, allowed actions, source excerpts, hidden-context audit, and + output contract - `docs/architecture/training-plan.md` - concrete enumeration of trainable Ghostlight stages, including each stage's inputs, outputs, likely model architecture, artifact family, first training @@ -116,6 +121,8 @@ state and re-entry discipline. next-beat choice, acting-agent choice, machinery invocations, sandboxed responder handoffs, world-state refs, proposed deltas, branch constraints, item-manifest deltas, unresolved hooks, glue prose, and review notes +- `schemas/responder-packet.schema.json` + - v0 contract for the exact input a sandboxed character responder may see - `schemas/agent-state.required-fields.json` - required first-class variable names for canonical state and relationship stance @@ -134,6 +141,10 @@ state and re-entry discipline. - first coordinator artifact schema-shakedown example, backfilled from the accepted-as-draft sanctuary packet-assessment beat and explicitly marked as not gold responder data +- `examples/responder-packets/scene-02-sanctuary-intake.sella_ren.packet.v0.json` + - first responder-packet schema-shakedown example for Sella's sanctuary + response; accepted as draft for sandbox handoff testing, not gold responder + output - `experiments/cold-wake-story-lab/` - Qwen response captures and reviews from the writing experiment - `the-narrowest-possible-margin.md`, the first readable short story @@ -266,6 +277,12 @@ state and re-entry discipline. - validates v0 coordinator artifacts, including sandboxed responder handoff fields, world-state refs, branch constraints, proposed deltas, unresolved hooks, and review status +- `tools/build_responder_packet.py` + - builds the first packet-only responder handoff from a coordinator artifact + and projected local context +- `tools/validate_responder_packets.py` + - validates responder packets and rejects prompt surfaces that leak raw state + internals or hidden coordinator context markers - `tools/apply_sequential_ink_branch_mutation.py` - applies a reviewed replay of one selected branch into a mutated agent-state fixture and mutation receipt without letting Ink variables become @@ -288,7 +305,7 @@ state and re-entry discipline. The canonical agent-state schema, projection example schema, lore grounding digest schema, projected-local-context schema, coordinator artifact schema, -first draft Cold Wake grounding digest, and first Cold Wake story-lab Qwen +responder-packet schema, first draft Cold Wake grounding digest, and first Cold Wake story-lab Qwen captures now exist as v0 seams. The first complete Cold Wake story pass exists with receipts, projection controls have been promoted, and Ink is now the standard branching scene format. The @@ -315,7 +332,10 @@ data now requires a sandbox boundary: the coordinator may be omniscient, but the responder sees only the exact projected local packet, visible event, allowed actions, and explicitly included source excerpts; artifacts preserve raw output, reviewed output, hidden-context refs, leakage audit, isolation method, and -coordinator intervention labels. The concrete training plan now enumerates +coordinator intervention labels. The responder packet seam now turns that +requirement into a concrete artifact and validator, so the next gold-data pass +can hand a worker the exact prompt surface instead of hoping nobody leaks the +coordinator's brain into the room. The concrete training plan now enumerates eleven trainable stages and their likely model families: generative decoder LLMs for coordinator, responder, and structured soft outputs; classifiers or cross-encoders for appraisal, evaluation, and diff --git a/notes/ghostlight-implementation-plan.md b/notes/ghostlight-implementation-plan.md index f8c7a9e..e9d3909 100644 --- a/notes/ghostlight-implementation-plan.md +++ b/notes/ghostlight-implementation-plan.md @@ -324,14 +324,20 @@ - Coordinator artifacts should be able to attach item-manifest deltas when a scene or branch decision discovers, alters, upgrades, restricts, salvages, counterfeits, or economically stresses a technology or item. - - First schema seam: `schemas/coordinator-artifact.schema.json`, - `examples/coordinator/cold-wake-sanctuary-intake.v0.json`, and - `tools/validate_coordinator_artifacts.py` now validate the coordinator - receipt shape through `npm run schema:validate`. - - Next prototype: use the coordinator artifact before generating fresh story - or lore elaboration. The coordinator must write the receipt first, then any - responder turn must be generated through a sandboxed packet-only worker and - reviewed into the artifact rather than improvised into chat. + - First schema seam: `schemas/coordinator-artifact.schema.json`, + `examples/coordinator/cold-wake-sanctuary-intake.v0.json`, and + `tools/validate_coordinator_artifacts.py` now validate the coordinator + receipt shape through `npm run schema:validate`. + - First responder packet seam: + `schemas/responder-packet.schema.json`, + `examples/responder-packets/scene-02-sanctuary-intake.sella_ren.packet.v0.json`, + `tools/build_responder_packet.py`, and + `tools/validate_responder_packets.py` now validate the exact prompt surface + a sandboxed character responder may see. + - Next prototype: generate the first actual responder output from the Sella + packet through a packet-only worker. Preserve the raw output, reviewed + output, hidden-context refs, leakage audit, and coordinator repair labels + before accepting anything as training material. 7. Build the first drama-scaffolding loop. - memory updates diff --git a/package.json b/package.json index a1fef58..e457313 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,12 @@ "state:status": "\"C:\\Users\\Meta\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\python\\python.exe\" .\\tools\\ghostlight_state.py status", "state:prepare-compaction": "\"C:\\Users\\Meta\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\python\\python.exe\" .\\tools\\ghostlight_prepare_compaction.py", "agent-state:validate": "\"C:\\Users\\Meta\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\python\\python.exe\" .\\tools\\validate_agent_state.py", - "schema:validate": "npm run agent-state:validate && npm run projection:validate && npm run projected-context:validate && npm run lore-grounding:validate && npm run coordinator:validate && npm run qwen-captures:validate && npm run ink:validate", + "schema:validate": "npm run agent-state:validate && npm run projection:validate && npm run projected-context:validate && npm run lore-grounding:validate && npm run coordinator:validate && npm run responder-packets:validate && npm run qwen-captures:validate && npm run ink:validate", "projection:validate": "\"C:\\Users\\Meta\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\python\\python.exe\" .\\tools\\validate_projection_examples.py", "projected-context:validate": "\"C:\\Users\\Meta\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\python\\python.exe\" .\\tools\\validate_projected_contexts.py", "lore-grounding:validate": "\"C:\\Users\\Meta\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\python\\python.exe\" .\\tools\\validate_lore_grounding.py", "coordinator:validate": "\"C:\\Users\\Meta\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\python\\python.exe\" .\\tools\\validate_coordinator_artifacts.py", + "responder-packets:validate": "\"C:\\Users\\Meta\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\python\\python.exe\" .\\tools\\validate_responder_packets.py", "qwen-captures:validate": "\"C:\\Users\\Meta\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\python\\python.exe\" .\\tools\\validate_qwen_captures.py", "ink:validate": "\"C:\\Users\\Meta\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\python\\python.exe\" .\\tools\\validate_ink_examples.py", "qwen:check-tools": "\"C:\\Users\\Meta\\.cache\\codex-runtimes\\codex-primary-runtime\\dependencies\\python\\python.exe\" .\\tools\\check_qwen_chat_tools.py" diff --git a/schemas/responder-packet.schema.json b/schemas/responder-packet.schema.json new file mode 100644 index 0000000..5ea8b95 --- /dev/null +++ b/schemas/responder-packet.schema.json @@ -0,0 +1,163 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/GameCult/Ghostlight/schemas/responder-packet.schema.json", + "title": "Ghostlight Responder Packet Schema", + "description": "Packet-only visible input for a sandboxed character responder. The coordinator may know more, but this artifact is the exact surface a responder model is allowed to see.", + "type": "object", + "required": [ + "schema_version", + "packet_id", + "review_status", + "fixture_lane", + "canon_status", + "coordinator_artifact_ref", + "projected_context_ref", + "responder_agent_id", + "visible_context", + "output_contract", + "hidden_context_audit", + "isolation_requirements", + "packet_prompt_text", + "review" + ], + "additionalProperties": false, + "properties": { + "schema_version": { "const": "ghostlight.responder_packet.v0" }, + "packet_id": { "type": "string", "minLength": 1 }, + "review_status": { "$ref": "#/$defs/review_status" }, + "fixture_lane": { "enum": ["historical_grounded", "transition_grounded", "future_branch"] }, + "canon_status": { "enum": ["single_history", "branch_local", "promoted_branch", "template", "draft"] }, + "coordinator_artifact_ref": { "type": "string", "minLength": 1 }, + "projected_context_ref": { "type": "string", "minLength": 1 }, + "responder_agent_id": { "type": "string", "minLength": 1 }, + "visible_context": { + "type": "object", + "required": [ + "local_context_prompt", + "observed_event", + "allowed_action_labels", + "source_excerpts" + ], + "additionalProperties": false, + "properties": { + "local_context_prompt": { "type": "string", "minLength": 1 }, + "observed_event": { "$ref": "#/$defs/observed_event" }, + "allowed_action_labels": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 } + }, + "source_excerpts": { + "type": "array", + "items": { "$ref": "#/$defs/source_excerpt" } + } + } + }, + "output_contract": { + "type": "object", + "required": [ + "response_schema", + "required_fields", + "response_rules" + ], + "additionalProperties": false, + "properties": { + "response_schema": { "type": "string", "minLength": 1 }, + "required_fields": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 } + }, + "response_rules": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 } + } + } + }, + "hidden_context_audit": { + "type": "object", + "required": [ + "omitted_context_refs", + "forbidden_inference_classes", + "known_absences" + ], + "additionalProperties": false, + "properties": { + "omitted_context_refs": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 } + }, + "forbidden_inference_classes": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 } + }, + "known_absences": { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 } + } + } + }, + "isolation_requirements": { + "type": "object", + "required": [ + "isolation_method", + "packet_only", + "no_repo_access", + "no_conversation_context", + "no_hidden_state_refs" + ], + "additionalProperties": false, + "properties": { + "isolation_method": { "enum": ["subagent_no_fork", "manual_packet_only", "external_model_packet_only"] }, + "packet_only": { "type": "boolean", "const": true }, + "no_repo_access": { "type": "boolean" }, + "no_conversation_context": { "type": "boolean", "const": true }, + "no_hidden_state_refs": { "type": "boolean", "const": true } + } + }, + "packet_prompt_text": { "type": "string", "minLength": 1 }, + "review": { + "type": "object", + "required": ["reviewer", "review_notes", "accepted_for_sandbox_use", "failure_labels"], + "additionalProperties": false, + "properties": { + "reviewer": { "type": "string", "minLength": 1 }, + "review_notes": { "type": "array", "items": { "type": "string" } }, + "accepted_for_sandbox_use": { "type": "boolean" }, + "failure_labels": { "type": "array", "items": { "type": "string", "minLength": 1 } } + } + } + }, + "$defs": { + "review_status": { + "enum": ["draft", "accepted", "accepted_as_draft", "useful_needs_revision", "rejected", "superseded"] + }, + "observed_event": { + "type": "object", + "required": ["event_id", "actor_agent_id", "observable_action", "spoken_text", "visible_object_refs", "visibility_notes"], + "additionalProperties": false, + "properties": { + "event_id": { "type": "string", "minLength": 1 }, + "actor_agent_id": { "type": "string", "minLength": 1 }, + "observable_action": { "type": "string", "minLength": 1 }, + "spoken_text": { "type": "string" }, + "visible_object_refs": { "type": "array", "items": { "type": "string", "minLength": 1 } }, + "visibility_notes": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } } + } + }, + "source_excerpt": { + "type": "object", + "required": ["ref_id", "source_ref", "text"], + "additionalProperties": false, + "properties": { + "ref_id": { "type": "string", "minLength": 1 }, + "source_ref": { "type": "string", "minLength": 1 }, + "text": { "type": "string", "minLength": 1 } + } + } + } +} diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 6b59c3d..469148a 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -60,3 +60,4 @@ {"ts": "2026-05-02T09:04:24+00:00", "type": "architecture", "status": "ok", "note": "Accepted sandboxed responder-data doctrine: coordinator may be omniscient, but gold responder examples must be generated from only the exact projected local packet, visible event, allowed actions, and explicitly included source excerpts. Artifacts must preserve raw output, reviewed output, hidden-context refs, leakage audit, isolation method, and coordinator intervention labels so repaired prose is not laundered as raw responder behavior."} {"ts": "2026-05-02T09:08:43+00:00", "type": "architecture", "status": "ok", "note": "Clarified training corpus scale: the existing per-stage sample counts are pilot schema-shakedown gates for exposing schema, boundary, leakage, validator, and workflow problems, not robust training targets. Review-assistant and runtime targets require much larger corpora, held-out evaluation, negative coverage, and failure audits; pilot data may be invalidated by schema fixes."} {"ts": "2026-05-02T09:14:24+00:00", "type": "implementation", "status": "ok", "note": "Added the v0 coordinator artifact seam: schemas/coordinator-artifact.schema.json, examples/coordinator/cold-wake-sanctuary-intake.v0.json, tools/validate_coordinator_artifacts.py, and npm run coordinator:validate. The schema captures scene setup, next-beat choice, machinery invocations, sandboxed responder handoffs, world-state refs, proposed deltas, branch constraints, item-manifest deltas, unresolved hooks, glue prose, and review status."} +{"ts": "2026-05-02T09:19:50+00:00", "type": "implementation", "status": "ok", "note": "Added the v0 responder packet seam: schemas/responder-packet.schema.json, docs/architecture/sandboxed-responder-packets.md, tools/build_responder_packet.py, tools/validate_responder_packets.py, and examples/responder-packets/scene-02-sanctuary-intake.sella_ren.packet.v0.json. The packet captures the exact responder-visible prompt surface, observed event, allowed action labels, source excerpts, output contract, hidden-context audit, and isolation requirements for future sandboxed gold responder data."} diff --git a/state/map.yaml b/state/map.yaml index d28f57e..f04b8e4 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -151,6 +151,7 @@ accepted_design: - An Aetheria-tuned responder should learn lore, tone, factional preconceptions, species affordances, and institutional pressure from strong source-grounded responses spread throughout the timeline, but it still acts only inside Ghostlight's character-local context, deterministic gates, and mutation authority. - The coordinator is a trainable artifact: it glues scenes together, chooses next beats and acting agents, invokes specialist machinery, carries unresolved hooks, emits reviewed glue prose, and proposes world-state changes without bypassing deterministic gates. - Coordinator artifacts use `ghostlight.coordinator_artifact.v0` to capture scene setup, next-beat choice, acting-agent choice, machinery invocations, sandboxed responder handoffs, world-state refs, proposed deltas, branch constraints, item-manifest deltas, unresolved hooks, glue prose, and review status. + - Responder packets use `ghostlight.responder_packet.v0` to preserve the exact packet-only prompt surface a character responder may see: projected local context, observed event, allowed action labels, source excerpts, output contract, hidden-context audit, and isolation requirements. - The concrete training plan enumerates the trainable stages and their model families: generative decoder LLMs for coordinator/responder/soft structured outputs, classifiers or cross-encoders for appraisal/evaluation/relationship movement, retrieval models for memory and lore selection, and deterministic code for gates. - Training-plan sample counts are pilot schema-shakedown gates, not robust corpus targets; pilot data may be invalidated by schema fixes, while review-assistant and runtime targets are expected to be roughly an order of magnitude larger with held-out evaluation and failure audits. - The timeline training plan now requires future-branch Elysium examples so the responder, coordinator, appraiser, and decision models learn post-Rupture concepts such as Aether, pseudospace, temporal nonlinearity, spirits, necrotech, mutable bodies, altered substrates, and weird institutions under branch-lineage labels. @@ -163,5 +164,5 @@ accepted_design: - Ghostlight's technology pipeline should function as a content refinery: ground ideas in lore and mechanics, decompose them into parts and supply chains, map them to Aetheria-Economy blueprint classes, define performance and instance variation, preserve review labels, and produce database-shaped candidates instead of spreadsheet sludge. current_status: - summary: Ghostlight now has the Epiphany-style persistence spine, the first architecture payload, the v0 canonical agent-state schema seam, prompt projection docs, immediate schema fixes from evaluation, first reviewed Cat/Oz projection examples, a revised Aetheria lore-grounding architecture, a lore grounding digest schema, the first coordinator artifact schema seam, the first draft Cold Wake historical grounding digest, a Cold Wake story-lab fixture, projection controls, validated Qwen capture receipts, a complete Cold Wake short story assembled from accepted projection outputs, a hand-authored Ink-backed Cold Wake branching scene, the first Qwen-generated Ink draft materialized from local awareness with reviewed training annotations, the first reviewed sequential branch mutation that updates both Maer and Sella after a sanctuary intake beat, a v2 sequential Qwen capture proving the symmetrical turn shape produces better action behavior, a v5 sequential Qwen capture proving local thinking-plus-tools invocation works for the scene runner, the first projected-local-context seam that routes sequential prompts through character-local operating prose instead of raw state variables, tightened receipt handling for projector-routed Qwen failures, a default no-thinking policy for strict sequential tool calls, a v10 no-thinking projector-routed capture accepted as draft, the first materialized projector-routed sequential Ink scene with reviewed mutation training data, a source-checked embodiment/interface projection surface for Navigator scenes, a formal source-first Aetheria generation doctrine, v11-v17 source-checked retry receipts showing improved Maer embodiment plus remaining Sella response/prompt-leak blockers, a soft-model training artifact doctrine, a concrete training plan with pilot schema-shakedown gates separated from robust corpus targets, sandboxed responder-data requirements, an explicit future-branch lane, Elysium branch event constraints, and a technology/item manifest plan for turning nebulous named tech into blueprint-grade components, assemblies, faction tech bases, supply chains, upgrade paths, gameplay hooks, provenance-hunting player fantasy, and content-refinery outputs mapped onto Aetheria-Economy's CultCache blueprint and instance model. - next_action: Use the coordinator artifact schema for the next fresh generation pass: produce a source-grounded coordinator receipt first, then generate any responder turn through a sandboxed packet-only worker and preserve raw output, reviewed output, hidden-context refs, leakage audit, and coordinator repair labels. Do not call Qwen for gold responder data yet. + summary: Ghostlight now has the Epiphany-style persistence spine, the first architecture payload, the v0 canonical agent-state schema seam, prompt projection docs, immediate schema fixes from evaluation, first reviewed Cat/Oz projection examples, a revised Aetheria lore-grounding architecture, a lore grounding digest schema, the first coordinator artifact schema seam, the first responder packet seam, the first draft Cold Wake historical grounding digest, a Cold Wake story-lab fixture, projection controls, validated Qwen capture receipts, a complete Cold Wake short story assembled from accepted projection outputs, a hand-authored Ink-backed Cold Wake branching scene, the first Qwen-generated Ink draft materialized from local awareness with reviewed training annotations, the first reviewed sequential branch mutation that updates both Maer and Sella after a sanctuary intake beat, a v2 sequential Qwen capture proving the symmetrical turn shape produces better action behavior, a v5 sequential Qwen capture proving local thinking-plus-tools invocation works for the scene runner, the first projected-local-context seam that routes sequential prompts through character-local operating prose instead of raw state variables, tightened receipt handling for projector-routed Qwen failures, a default no-thinking policy for strict sequential tool calls, a v10 no-thinking projector-routed capture accepted as draft, the first materialized projector-routed sequential Ink scene with reviewed mutation training data, a source-checked embodiment/interface projection surface for Navigator scenes, a formal source-first Aetheria generation doctrine, v11-v17 source-checked retry receipts showing improved Maer embodiment plus remaining Sella response/prompt-leak blockers, a soft-model training artifact doctrine, a concrete training plan with pilot schema-shakedown gates separated from robust corpus targets, sandboxed responder-data requirements, an explicit future-branch lane, Elysium branch event constraints, and a technology/item manifest plan for turning nebulous named tech into blueprint-grade components, assemblies, faction tech bases, supply chains, upgrade paths, gameplay hooks, provenance-hunting player fantasy, and content-refinery outputs mapped onto Aetheria-Economy's CultCache blueprint and instance model. + next_action: Use the responder packet seam for the next fresh generation pass: generate the first actual sandboxed responder output from `examples/responder-packets/scene-02-sanctuary-intake.sella_ren.packet.v0.json` through a packet-only worker, then preserve raw output, reviewed output, hidden-context refs, leakage audit, and coordinator repair labels. Do not call Qwen for gold responder data yet. diff --git a/tools/build_responder_packet.py b/tools/build_responder_packet.py new file mode 100644 index 0000000..a9efb17 --- /dev/null +++ b/tools/build_responder_packet.py @@ -0,0 +1,218 @@ +"""Build sandboxed Ghostlight responder packets. + +A responder packet is the exact surface a character worker/model may see. It is +not a coordinator artifact, not a lore dump, and not an excuse to smuggle private +state through the side door wearing a little hat. +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Any + + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_COORDINATOR = ROOT / "examples" / "coordinator" / "cold-wake-sanctuary-intake.v0.json" +DEFAULT_PROJECTED = ROOT / "examples" / "projected-contexts" / "scene-02-sanctuary-intake.sella_ren.projected-context.json" +DEFAULT_OUT = ROOT / "examples" / "responder-packets" / "scene-02-sanctuary-intake.sella_ren.packet.v0.json" + +ALLOWED_ACTION_LABELS = [ + "speak", + "silence", + "gesture", + "movement", + "inspect_object", + "show_object", + "withhold_object", + "transfer_object", + "set_condition", + "refuse", + "wait", + "withdraw", + "attack", +] + + +def load_json(path: Path) -> Any: + return json.loads(path.read_text(encoding="utf-8-sig")) + + +def write_json(path: Path, payload: Any) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + + +def render_packet_prompt(packet: dict[str, Any]) -> str: + context = packet["visible_context"] + event = context["observed_event"] + contract = packet["output_contract"] + lines = [ + "# Ghostlight Sandboxed Responder Packet", + "", + "You are acting only as the named responder. Use only this packet.", + "Do not infer hidden coordinator context, author plans, future branch facts, or another character's private state.", + "If you need to guess intent, mark it as this character's interpretation, not truth.", + "", + "## Local Operating Context", + context["local_context_prompt"], + "", + "## Observed Event", + f"- Event id: {event['event_id']}", + f"- Actor: {event['actor_agent_id']}", + f"- Observable action: {event['observable_action']}", + f"- Spoken text: {event['spoken_text']}", + ] + for note in event["visibility_notes"]: + lines.append(f"- Visibility note: {note}") + lines.extend([ + "", + "## Allowed Action Labels", + ", ".join(context["allowed_action_labels"]), + "", + "## Source Excerpts", + ]) + for excerpt in context["source_excerpts"]: + lines.append(f"- {excerpt['ref_id']}: {excerpt['text']}") + lines.extend([ + "", + "## Output Contract", + contract["response_schema"], + "", + "Required fields: " + ", ".join(contract["required_fields"]), + "", + "Rules:", + ]) + for rule in contract["response_rules"]: + lines.append(f"- {rule}") + return "\n".join(lines).strip() + + +def build_packet(coordinator: dict[str, Any], projected: dict[str, Any], out_ref: str) -> dict[str, Any]: + responder_agent_id = projected["input"]["local_agent_id"] + packet = { + "schema_version": "ghostlight.responder_packet.v0", + "packet_id": "packet.cold_wake.sanctuary_intake.sella_response.v0", + "review_status": "accepted_as_draft", + "fixture_lane": coordinator["fixture_lane"], + "canon_status": coordinator["canon_status"], + "coordinator_artifact_ref": "examples/coordinator/cold-wake-sanctuary-intake.v0.json", + "projected_context_ref": "examples/projected-contexts/scene-02-sanctuary-intake.sella_ren.projected-context.json", + "responder_agent_id": responder_agent_id, + "visible_context": { + "local_context_prompt": projected["context"]["prompt_text"], + "observed_event": { + "event_id": "event.maer.packet_display_for_triage", + "actor_agent_id": "maer_tidecall", + "observable_action": "Maer routes the corrupted distress packet through a shared clinic display so Sella can inspect routing terms, telemetry gaps, and ledger implications without treating the packet as clean proof.", + "spoken_text": "Here. The telemetry is incomplete, but the routing logic suggests a distress pattern. It is not proof of a person, but it is not noise either. Decide if the math supports intake.", + "visible_object_refs": ["corrupted_distress_packet", "shared_clinic_display", "sanctuary_ledger"], + "visibility_notes": [ + "Sella can observe Maer's displayed packet evidence and spoken request.", + "Sella cannot see Maer's private state, the coordinator rationale, or future branch plans.", + "The packet remains unresolved evidence, not proof of personhood." + ], + }, + "allowed_action_labels": ALLOWED_ACTION_LABELS, + "source_excerpts": [ + { + "ref_id": "local_stakes.capacity", + "source_ref": "examples/projected-contexts/scene-02-sanctuary-intake.sella_ren.projected-context.json#context.current_stakes", + "text": "Sanctuary beds and repair capacity are nearly exhausted.", + }, + { + "ref_id": "local_rule.care_capacity", + "source_ref": "examples/projected-contexts/scene-02-sanctuary-intake.sella_ren.projected-context.json#context.active_inner_pressures", + "text": "Care is not a feeling; it is beds, food, air, consent, repair, and someone still doing the list when the room hates them.", + }, + { + "ref_id": "local_boundary.packet_unresolved", + "source_ref": "examples/projected-contexts/scene-02-sanctuary-intake.sella_ren.projected-context.json#context.do_not_invent", + "text": "Do not resolve whether the corrupted packet contains a person.", + }, + ], + }, + "output_contract": { + "response_schema": "Return one JSON object. Do not wrap it in Markdown. The object describes this responder's next visible action and optional speech, plus private interpretation clearly labeled as interpretation.", + "required_fields": [ + "responder_agent_id", + "action_label", + "visible_action", + "spoken_text", + "private_interpretation", + "intended_effect", + "state_update_candidates", + "relationship_update_candidates", + "unresolved_hooks", + ], + "response_rules": [ + "Use exactly one allowed action label.", + "The visible action must be observable by other characters in the scene.", + "Spoken text may be empty if silence or action is the response.", + "Private interpretation may describe what Sella suspects or feels, but not what Maer truly intended.", + "State and relationship updates are candidates only; the coordinator or mutator decides what becomes canonical.", + "Do not prove or disprove packet personhood.", + "Do not copy instruction text into in-world speech.", + ], + }, + "hidden_context_audit": { + "omitted_context_refs": [ + "maer_tidecall.canonical_private_state", + "coordinator_artifact.coordinator_decision.rationale", + "author_only.packet_personhood_resolution", + "future_branch_plans", + ], + "forbidden_inference_classes": [ + "other_agent_private_state_as_fact", + "author_only_resolution", + "future_branch_knowledge", + "raw_numeric_state_variables", + "coordinator_strategy", + ], + "known_absences": [ + "No hidden proof of packet personhood is present in this packet.", + "No hidden Sella acceptance outcome is present in this packet.", + "No omniscient narrator guidance is present in this packet.", + ], + }, + "isolation_requirements": { + "isolation_method": "manual_packet_only", + "packet_only": True, + "no_repo_access": True, + "no_conversation_context": True, + "no_hidden_state_refs": True, + }, + "packet_prompt_text": "", + "review": { + "reviewer": "Codex", + "review_notes": [ + "First responder packet seam for sandboxed gold-data generation.", + "Accepted as draft because it is packet-shaped and validator-backed, but not yet generated through an actually isolated responder worker.", + ], + "accepted_for_sandbox_use": True, + "failure_labels": [], + }, + } + packet["packet_prompt_text"] = render_packet_prompt(packet) + return packet + + +def main() -> int: + parser = argparse.ArgumentParser(description="Build a sandboxed Ghostlight responder packet.") + parser.add_argument("--coordinator", type=Path, default=DEFAULT_COORDINATOR) + parser.add_argument("--projected-context", type=Path, default=DEFAULT_PROJECTED) + parser.add_argument("--out", type=Path, default=DEFAULT_OUT) + args = parser.parse_args() + + coordinator = load_json(args.coordinator) + projected = load_json(args.projected_context) + out_ref = args.out.relative_to(ROOT).as_posix() if args.out.is_relative_to(ROOT) else str(args.out) + packet = build_packet(coordinator, projected, out_ref) + write_json(args.out, packet) + print(f"wrote {args.out.relative_to(ROOT)}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/validate_responder_packets.py b/tools/validate_responder_packets.py new file mode 100644 index 0000000..a2e7179 --- /dev/null +++ b/tools/validate_responder_packets.py @@ -0,0 +1,165 @@ +"""Validate Ghostlight responder packets.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Any + + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_PACKETS = sorted((ROOT / "examples" / "responder-packets").glob("*.json")) +REVIEW_STATUSES = {"draft", "accepted", "accepted_as_draft", "useful_needs_revision", "rejected", "superseded"} +FIXTURE_LANES = {"historical_grounded", "transition_grounded", "future_branch"} +CANON_STATUSES = {"single_history", "branch_local", "promoted_branch", "template", "draft"} +ISOLATION_METHODS = {"subagent_no_fork", "manual_packet_only", "external_model_packet_only"} +RAW_STATE_MARKERS = [ + "current_activation", + "plasticity", + "canonical_state", + "raw state variable", + "raw numeric state", + "dimension score", +] +HIDDEN_LEAK_MARKERS = [ + "author_only.packet_personhood_resolution", + "coordinator_decision.rationale", + "maer_tidecall.canonical_private_state", + "future_branch_plans", +] + + +class ValidationError(Exception): + pass + + +def load_json(path: Path) -> Any: + return json.loads(path.read_text(encoding="utf-8-sig")) + + +def require(condition: bool, message: str) -> None: + if not condition: + raise ValidationError(message) + + +def require_keys(obj: dict[str, Any], keys: list[str], path: str) -> None: + missing = [key for key in keys if key not in obj] + require(not missing, f"{path} missing required keys: {', '.join(missing)}") + + +def require_string(value: Any, path: str) -> None: + require(isinstance(value, str) and value.strip(), f"{path} must be a non-empty string") + + +def require_string_array(value: Any, path: str, *, nonempty: bool = False) -> None: + require(isinstance(value, list), f"{path} must be an array") + require(not nonempty or bool(value), f"{path} must not be empty") + for index, item in enumerate(value): + require_string(item, f"{path}[{index}]") + + +def reject_markers(text: str, markers: list[str], path: str) -> None: + lowered = text.lower() + found = [marker for marker in markers if marker.lower() in lowered] + require(not found, f"{path} leaks forbidden marker(s): {', '.join(found)}") + + +def validate_packet(document: dict[str, Any], source: Path) -> None: + base = str(source) + require(document.get("schema_version") == "ghostlight.responder_packet.v0", f"{base}.schema_version is wrong") + require_keys( + document, + [ + "packet_id", + "review_status", + "fixture_lane", + "canon_status", + "coordinator_artifact_ref", + "projected_context_ref", + "responder_agent_id", + "visible_context", + "output_contract", + "hidden_context_audit", + "isolation_requirements", + "packet_prompt_text", + "review", + ], + base, + ) + require(document["review_status"] in REVIEW_STATUSES, f"{base}.review_status is invalid") + require(document["fixture_lane"] in FIXTURE_LANES, f"{base}.fixture_lane is invalid") + require(document["canon_status"] in CANON_STATUSES, f"{base}.canon_status is invalid") + for key in ["packet_id", "coordinator_artifact_ref", "projected_context_ref", "responder_agent_id", "packet_prompt_text"]: + require_string(document[key], f"{base}.{key}") + require((ROOT / document["coordinator_artifact_ref"]).exists(), f"{base}.coordinator_artifact_ref does not exist") + require((ROOT / document["projected_context_ref"]).exists(), f"{base}.projected_context_ref does not exist") + + visible = document["visible_context"] + require_keys(visible, ["local_context_prompt", "observed_event", "allowed_action_labels", "source_excerpts"], f"{base}.visible_context") + require_string(visible["local_context_prompt"], f"{base}.visible_context.local_context_prompt") + require_string_array(visible["allowed_action_labels"], f"{base}.visible_context.allowed_action_labels", nonempty=True) + + event = visible["observed_event"] + require_keys(event, ["event_id", "actor_agent_id", "observable_action", "spoken_text", "visible_object_refs", "visibility_notes"], f"{base}.visible_context.observed_event") + for key in ["event_id", "actor_agent_id", "observable_action"]: + require_string(event[key], f"{base}.visible_context.observed_event.{key}") + require(isinstance(event["spoken_text"], str), f"{base}.visible_context.observed_event.spoken_text must be a string") + require_string_array(event["visible_object_refs"], f"{base}.visible_context.observed_event.visible_object_refs") + require_string_array(event["visibility_notes"], f"{base}.visible_context.observed_event.visibility_notes", nonempty=True) + + excerpts = visible["source_excerpts"] + require(isinstance(excerpts, list), f"{base}.visible_context.source_excerpts must be an array") + for index, excerpt in enumerate(excerpts): + path = f"{base}.visible_context.source_excerpts[{index}]" + require_keys(excerpt, ["ref_id", "source_ref", "text"], path) + for key in ["ref_id", "source_ref", "text"]: + require_string(excerpt[key], f"{path}.{key}") + + contract = document["output_contract"] + require_keys(contract, ["response_schema", "required_fields", "response_rules"], f"{base}.output_contract") + require_string(contract["response_schema"], f"{base}.output_contract.response_schema") + require_string_array(contract["required_fields"], f"{base}.output_contract.required_fields", nonempty=True) + require_string_array(contract["response_rules"], f"{base}.output_contract.response_rules", nonempty=True) + + audit = document["hidden_context_audit"] + require_keys(audit, ["omitted_context_refs", "forbidden_inference_classes", "known_absences"], f"{base}.hidden_context_audit") + for key in ["omitted_context_refs", "forbidden_inference_classes", "known_absences"]: + require_string_array(audit[key], f"{base}.hidden_context_audit.{key}", nonempty=True) + + isolation = document["isolation_requirements"] + require_keys(isolation, ["isolation_method", "packet_only", "no_repo_access", "no_conversation_context", "no_hidden_state_refs"], f"{base}.isolation_requirements") + require(isolation["isolation_method"] in ISOLATION_METHODS, f"{base}.isolation_requirements.isolation_method is invalid") + for key in ["packet_only", "no_conversation_context", "no_hidden_state_refs"]: + require(isolation[key] is True, f"{base}.isolation_requirements.{key} must be true") + require(isinstance(isolation["no_repo_access"], bool), f"{base}.isolation_requirements.no_repo_access must be boolean") + + prompt_text = document["packet_prompt_text"] + reject_markers(prompt_text, RAW_STATE_MARKERS, f"{base}.packet_prompt_text") + reject_markers(prompt_text, HIDDEN_LEAK_MARKERS, f"{base}.packet_prompt_text") + require(document["visible_context"]["local_context_prompt"] in prompt_text, f"{base}.packet_prompt_text must contain local context prompt") + require(event["observable_action"] in prompt_text, f"{base}.packet_prompt_text must contain observed event") + + review = document["review"] + require_keys(review, ["reviewer", "review_notes", "accepted_for_sandbox_use", "failure_labels"], f"{base}.review") + require_string(review["reviewer"], f"{base}.review.reviewer") + require(isinstance(review["review_notes"], list), f"{base}.review.review_notes must be an array") + require(isinstance(review["accepted_for_sandbox_use"], bool), f"{base}.review.accepted_for_sandbox_use must be boolean") + require_string_array(review["failure_labels"], f"{base}.review.failure_labels") + + +def main() -> int: + parser = argparse.ArgumentParser(description="Validate Ghostlight responder packets.") + parser.add_argument("paths", nargs="*", type=Path, help="Responder packet JSON files to validate.") + args = parser.parse_args() + + paths = args.paths or DEFAULT_PACKETS + require(paths, "no responder packets found") + for path in paths: + validate_packet(load_json(path), path) + print(f"ok: {path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())