File size: 17,344 Bytes
3d46076
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
"""LLM control plane (STAGE K): typed, schema-validated commands.



The LLM scientist may SPAWN, START, PAUSE, STOP, SAVE, LOAD, CONFIGURE,

REQUEST_EXPERIMENTS and PROPOSE hypotheses/curricula — through this strictly

validated command surface only. No shell, no code mutation, no direct weight

or memory edits, no fabricated results. Every execution is logged with

provenance; every rejection states the exact reason.

"""
import hashlib
import json
import re
import time
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional

COMMAND_SCHEMA_VERSION = "control_v1"

# command -> {required params: {name: type}, optional params, constraints}
COMMAND_SPECS: Dict[str, Dict[str, Any]] = {
    "SPAWN_POPULATION": {
        "required": {"size": int},
        "optional": {"world_seed": int, "config_name": str},
        "constraints": {"size": (1, 64)},
    },
    "START_RUN": {"required": {}, "optional": {"ticks": int},
                  "constraints": {"ticks": (1, 10000)}},
    "PAUSE_RUN": {"required": {}, "optional": {}, "constraints": {}},
    "STOP_RUN": {"required": {}, "optional": {}, "constraints": {}},
    "SAVE_CHECKPOINT": {"required": {"name": str}, "optional": {},
                        "constraints": {"name": (1, 200)}},
    "LOAD_CHECKPOINT": {"required": {"name": str}, "optional": {},
                        "constraints": {"name": (1, 200)}},
    "SET_WORLD_CONFIG": {
        "required": {"n_resources": int},
        "optional": {"n_hazards": int, "regrow_interval": int},
        "constraints": {"n_resources": (4, 512), "n_hazards": (0, 64),
                        "regrow_interval": (1, 100)},
    },
    "SET_EVOLUTION_CONFIG": {
        "required": {"offspring_per_generation": int},
        "optional": {"reproduction_mode": str},
        "constraints": {"offspring_per_generation": (0, 32)},
        "enum": {"reproduction_mode": ["sexual", "asexual"]},
    },
    "REQUEST_EXPERIMENT": {
        "required": {"experiment_type": str, "seed": int},
        "optional": {"ticks": int, "population_size": int},
        "constraints": {"seed": (0, 2 ** 31), "ticks": (1, 2000),
                        "population_size": (1, 32)},
        "enum": {"experiment_type": ["baseline", "ablation_no_teaching",
                                     "ablation_no_growth", "comparison"]},
    },
    "REQUEST_COMPARISON": {
        "required": {"experiment_a": str, "experiment_b": str},
        "optional": {}, "constraints": {},
    },
    "REQUEST_REPLAY": {"required": {"checkpoint_name": str}, "optional": {"ticks": int},
                       "constraints": {"ticks": (1, 2000)}},
    "PROPOSE_HYPOTHESIS": {
        "required": {"text": str, "based_on_experiments": list},
        "optional": {}, "constraints": {"text": (1, 2000)},
    },
    "PROPOSE_TASK": {
        "required": {"description": str, "success_criterion": str},
        "optional": {}, "constraints": {"description": (1, 500),
                                        "success_criterion": (1, 500)},
    },
    "PROPOSE_CURRICULUM": {
        "required": {"stages": list},
        "optional": {}, "constraints": {"stages": (1, 8)},
    },
}

# Capability allowlist: role -> commands the role may invoke. Typed dispatch is
# the security boundary (text params are data, never executed). Identifier
# params (checkpoint names, experiment ids, ...) must additionally match
# _IDENTIFIER_RE; free-text fields (hypothesis text, descriptions, curriculum
# stages) are never scanned and never executed.
CAPABILITY_ROLES: Dict[str, frozenset] = {
    "llm-scientist": frozenset(COMMAND_SPECS.keys()),
    "viewer": frozenset({"REQUEST_COMPARISON", "REQUEST_REPLAY", "PROPOSE_HYPOTHESIS"}),
}

# Identifier-shaped params: strict allowlist, no shell metachars possible.
_IDENTIFIER_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,199}")
IDENTIFIER_PARAMS = {"name", "checkpoint_name", "experiment_a", "experiment_b",
                     "config_name", "reproduction_mode", "experiment_type"}

# Deprecated: whole-blob substring blacklists were fragile (false positives on
# legitimate scientific text, false negatives via obfuscation). Kept as an
# empty tuple for backward-compatible imports; enforcement is capability-based.
FORBIDDEN_SUBSTRINGS: tuple = ()


@dataclass
class CommandEnvelope:
    command: str
    params: Dict[str, Any] = field(default_factory=dict)
    requested_by: str = "llm-scientist"
    schema_version: str = COMMAND_SCHEMA_VERSION
    timestamp: float = 0.0

    def to_dict(self) -> Dict[str, Any]:
        return {"command": self.command, "params": self.params,
                "requested_by": self.requested_by,
                "schema_version": self.schema_version,
                "timestamp": self.timestamp or time.time()}


def validate_envelope(envelope: Any, role: str = "llm-scientist") -> tuple:
    """Returns (ok, error). Structural + schema + capability validation, no execution."""
    if not isinstance(envelope, CommandEnvelope):
        return False, "payload is not a CommandEnvelope"
    if envelope.schema_version != COMMAND_SCHEMA_VERSION:
        return False, f"unsupported schema version {envelope.schema_version!r}"
    spec = COMMAND_SPECS.get(envelope.command)
    if spec is None:
        return False, f"unknown command {envelope.command!r}"
    allowed_cmds = CAPABILITY_ROLES.get(role, frozenset())
    if envelope.command not in allowed_cmds:
        return False, f"command {envelope.command!r} not permitted for role {role!r}"
    params = envelope.params
    if not isinstance(params, dict):
        return False, "params must be a dict"
    for name, typ in spec["required"].items():
        if name not in params:
            return False, f"missing required param {name!r}"
        if typ is int and isinstance(params[name], bool):
            return False, f"param {name!r} must be int"
        if typ is int and not isinstance(params[name], int):
            return False, f"param {name!r} must be int"
        if typ is str and not isinstance(params[name], str):
            return False, f"param {name!r} must be str"
        if typ is list and not isinstance(params[name], list):
            return False, f"param {name!r} must be list"
    allowed = set(spec["required"]) | set(spec["optional"])
    extra = set(params) - allowed
    if extra:
        return False, f"unknown params {sorted(extra)}"
    for name, (lo, hi) in spec["constraints"].items():
        if name in params:
            v = params[name]
            if isinstance(v, str):
                if not (lo <= len(v) <= hi):
                    return False, f"param {name!r} length outside [{lo},{hi}]"
            elif isinstance(v, list):
                if not (lo <= len(v) <= hi):
                    return False, f"param {name!r} list length outside [{lo},{hi}]"
            elif not (lo <= v <= hi):
                return False, f"param {name!r}={v} outside [{lo},{hi}]"
    for name, allowed_vals in spec.get("enum", {}).items():
        if name in params and params[name] not in allowed_vals:
            return False, f"param {name!r} must be one of {allowed_vals}"
    # Capability-based identifier guard: identifier params must match the
    # strict allowlist (no shell metachars can pass). Free-text params
    # (text/description/success_criterion/stages) are data, never executed,
    # and are intentionally NOT scanned.
    for name in IDENTIFIER_PARAMS:
        if name in params and isinstance(params[name], str):
            if _IDENTIFIER_RE.fullmatch(params[name]) is None:
                return False, f"param {name!r} is not a valid identifier"
    return True, ""


class ResearchRuntime:
    """Headless research facade the control plane operates on. Owns the

    population, checkpoints and the experiment ledger. NO source mutation."""

    def __init__(self, experiment_seed: int = 42):
        self.experiment_seed = int(experiment_seed)
        self.population = None
        self.running = False
        self.checkpoints: Dict[str, Dict[str, Any]] = {}
        self.experiment_ledger: Dict[str, Dict[str, Any]] = {}
        self.hypotheses: List[Dict[str, Any]] = []
        self.curricula: List[Dict[str, Any]] = []
        self.tasks: List[Dict[str, Any]] = []
        self.execution_log: List[Dict[str, Any]] = []
        self._ticks_target = 0
        self._ticks_done = 0

    # ---- operations invoked by the control plane ----
    def op_spawn_population(self, size: int, world_seed: int = 47, **_):
        from src.common.determinism import SeedBundle
        from src.population.population import Population
        from src.connectome.types import GraphMode
        if self.population is not None:
            return {"status": "FAILED", "reason": "population already exists; STOP+RESET first"}
        seeds = SeedBundle(experiment_seed=self.experiment_seed,
                           generation_seed=self.experiment_seed + 1,
                           organism_seed=self.experiment_seed + 2,
                           development_seed=self.experiment_seed + 3,
                           mutation_seed=self.experiment_seed + 4,
                           world_seed=world_seed,
                           teacher_seed=self.experiment_seed + 6)
        self.population = Population(size, seeds, GraphMode.SYNTHETIC_TEST, 32,
                                     experiment_seed=self.experiment_seed,
                                     autonomy_mode=True, genome_version="2.0")
        return {"status": "EXECUTED", "population_size": size,
                "population_hash": self.population.population_hash()}

    def op_start_run(self, ticks: int = 10, **_):
        if self.population is None:
            return {"status": "FAILED", "reason": "no population"}
        self.running = True
        self._ticks_target = int(ticks)
        self._ticks_done = 0
        self.population.step(int(ticks))
        self._ticks_done = int(ticks)
        self.running = False
        return {"status": "EXECUTED", "ticks_run": self._ticks_done,
                "population_hash": self.population.population_hash()}

    def op_pause_run(self, **_):
        self.running = False
        return {"status": "EXECUTED", "paused": True}

    def op_stop_run(self, **_):
        self.running = False
        return {"status": "EXECUTED", "stopped": True,
                "tick": self.population.tick if self.population else 0}

    def op_save_checkpoint(self, name: str, **_):
        if self.population is None:
            return {"status": "FAILED", "reason": "no population"}
        self.checkpoints[name] = self.population.snapshot()
        return {"status": "EXECUTED", "checkpoint": name,
                "population_hash": self.population.population_hash()}

    def op_load_checkpoint(self, name: str, **_):
        if name not in self.checkpoints:
            return {"status": "FAILED", "reason": f"unknown checkpoint {name!r}"}
        from src.common.determinism import SeedBundle
        from src.population.population import Population
        seeds = SeedBundle(experiment_seed=self.experiment_seed,
                           generation_seed=self.experiment_seed + 1,
                           organism_seed=self.experiment_seed + 2,
                           development_seed=self.experiment_seed + 3,
                           mutation_seed=self.experiment_seed + 4,
                           world_seed=self.experiment_seed + 5,
                           teacher_seed=self.experiment_seed + 6)
        self.population = Population.restore(self.checkpoints[name], seeds)
        return {"status": "EXECUTED", "checkpoint": name,
                "population_hash": self.population.population_hash()}

    def op_request_experiment(self, experiment_type: str, seed: int,

                              ticks: int = 20, population_size: int = 4, **_):
        from src.common.determinism import SeedBundle
        from src.population.population import Population
        from src.connectome.types import GraphMode
        exp_id = f"exp-{experiment_type}-{seed}"
        seeds = SeedBundle(experiment_seed=seed, generation_seed=seed + 1,
                           organism_seed=seed + 2, development_seed=seed + 3,
                           mutation_seed=seed + 4, world_seed=seed + 5,
                           teacher_seed=seed + 6)
        pop = Population(population_size, seeds, GraphMode.SYNTHETIC_TEST, 32,
                         experiment_seed=seed, autonomy_mode=(experiment_type != "baseline"),
                         genome_version="2.0")
        pop.step(int(ticks))
        pop.reproduce(2)
        result = {
            "experiment_id": exp_id, "type": experiment_type, "seed": seed,
            "ticks": ticks, "population_size": population_size,
            "final_population_hash": pop.population_hash(),
            "teaching_sessions": len(pop.teaching_sessions),
            "living": len(pop.living()), "total_organisms": len(pop.organisms),
            "generations": sorted({o.generation for o in pop.organisms}),
        }
        self.experiment_ledger[exp_id] = result
        return {"status": "EXECUTED", "result": result}

    def op_request_comparison(self, experiment_a: str, experiment_b: str, **_):
        ra, rb = self.experiment_ledger.get(experiment_a), self.experiment_ledger.get(experiment_b)
        if ra is None or rb is None:
            return {"status": "FAILED", "reason": "unknown experiment id(s)"}
        comparison = {
            "a": experiment_a, "b": experiment_b,
            "hash_equal": ra["final_population_hash"] == rb["final_population_hash"],
            "teaching_sessions": {"a": ra["teaching_sessions"], "b": rb["teaching_sessions"]},
            "living": {"a": ra["living"], "b": rb["living"]},
            "generations": {"a": ra["generations"], "b": rb["generations"]},
        }
        return {"status": "EXECUTED", "comparison": comparison}

    def op_propose_hypothesis(self, text: str, based_on_experiments: list, **_):
        known = [e for e in based_on_experiments if e in self.experiment_ledger]
        unknown = [e for e in based_on_experiments if e not in self.experiment_ledger]
        # Deterministic research identity (V4 §34): content + sequence, never wall-clock.
        hid = hashlib.sha256(
            f"{text}|{len(self.hypotheses)}|{self.experiment_seed}".encode()).hexdigest()[:12]
        rec = {"hypothesis_id": hid, "text": text, "based_on_experiments": known,
               "unknown_references": unknown, "status": "HYPOTHESIS"}
        self.hypotheses.append(rec)
        return {"status": "EXECUTED", "hypothesis": rec}

    def op_propose_task(self, description: str, success_criterion: str, **_):
        tid = hashlib.sha256(
            f"{description}|{success_criterion}|{len(self.tasks)}|{self.experiment_seed}"
            .encode()).hexdigest()[:12]
        rec = {"task_id": tid, "description": description,
               "success_criterion": success_criterion, "status": "PROPOSED"}
        self.tasks.append(rec)
        return {"status": "EXECUTED", "task": rec}

    def op_propose_curriculum(self, stages: list, **_):
        if not all(isinstance(s, str) for s in stages):
            return {"status": "REJECTED", "reason": "curriculum stages must be strings"}
        cid = hashlib.sha256(
            f"{'|'.join(stages)}|{len(self.curricula)}|{self.experiment_seed}"
            .encode()).hexdigest()[:12]
        rec = {"curriculum_id": cid, "stages": stages, "status": "PROPOSED"}
        self.curricula.append(rec)
        return {"status": "EXECUTED", "curriculum": rec}


class ControlPlane:
    """Validates and executes LLM command envelopes against a ResearchRuntime."""

    def __init__(self, runtime: Optional[ResearchRuntime] = None,

                 role: str = "llm-scientist"):
        self.runtime = runtime if runtime is not None else ResearchRuntime()
        self.role = role

    def execute(self, envelope: Any) -> Dict[str, Any]:
        ok, err = validate_envelope(envelope, role=self.role)
        if not ok:
            result = {"status": "REJECTED", "reason": err,
                      "command": getattr(envelope, "command", str(envelope)[:80])}
            self.runtime.execution_log.append({**result, "ts": time.time()})
            return result
        op = getattr(self.runtime, f"op_{envelope.command.lower()}", None)
        if op is None:
            result = {"status": "REJECTED", "reason": "command has no executor",
                      "command": envelope.command}
        else:
            try:
                result = op(**envelope.params)
            except Exception as e:  # noqa: BLE001
                result = {"status": "FAILED", "command": envelope.command,
                          "reason": f"{type(e).__name__}: {e}"}
        self.runtime.execution_log.append({**result, "command": envelope.command,
                                           "ts": time.time()})
        return result