feat: plan 18
This commit is contained in:
@@ -42,6 +42,8 @@ TENANT_STORE = os.path.join(BIN, "tenant-store.sh")
|
||||
TENANT_CRYPT = os.path.join(BIN, "tenant-crypt.sh")
|
||||
KILL_SWITCH = os.path.join(BIN, "kill-switch.sh")
|
||||
COST_SPIKE = os.path.join(BIN, "cost-spike-detect.sh")
|
||||
MODEL_ROUTER = os.path.join(BIN, "model-router.sh")
|
||||
MODEL_PROVIDERS = os.path.join(ROOT, "packages", "casan-harness", "config", "model-providers.yaml")
|
||||
|
||||
|
||||
def state_root() -> str:
|
||||
@@ -346,9 +348,9 @@ def certify_operator_draft(args, router, binding):
|
||||
}, (0 if certified else 3)
|
||||
|
||||
|
||||
def render_codegen_draft(args, binding) -> str:
|
||||
def render_codegen_draft(args, binding, body=None, meta=None) -> str:
|
||||
fn = "generated_chat_draft"
|
||||
return "\n".join([
|
||||
scaffold = "\n".join([
|
||||
"# CODEGEN_DRAFT",
|
||||
"# GENERATED_BY_CASAN_CHAT",
|
||||
f"# agent={binding.get('agent_selected')}",
|
||||
@@ -363,6 +365,74 @@ def render_codegen_draft(args, binding) -> str:
|
||||
" }",
|
||||
"",
|
||||
])
|
||||
if body:
|
||||
meta = meta or {}
|
||||
scaffold += "\n".join([
|
||||
"# === MODEL_DRAFT BEGIN (review-only; never auto-applied) ===",
|
||||
f"# provider={meta.get('provider')} model={meta.get('model')}",
|
||||
body,
|
||||
"# === MODEL_DRAFT END ===",
|
||||
"",
|
||||
])
|
||||
return scaffold
|
||||
|
||||
|
||||
def _model_codegen_body(args):
|
||||
"""Item 4: full model-router CODEGEN path. Offline-first — model synthesis is
|
||||
gated behind CASAN_CHAT_MODEL_MODE=model; any failure falls SAFE back to the
|
||||
deterministic scaffold. The generated code stays draft-only and is still run
|
||||
through artifact-scan + Plan-17 loop certification by the caller.
|
||||
"""
|
||||
mode = os.environ.get("CASAN_CHAT_MODEL_MODE", "off").strip().lower()
|
||||
if mode != "model":
|
||||
return None, {"mode": "deterministic", "reason": "model_mode_off"}
|
||||
try:
|
||||
cfg = json.load(open(os.environ.get("CASAN_MODEL_PROVIDERS_FILE") or MODEL_PROVIDERS, encoding="utf-8"))
|
||||
except Exception:
|
||||
return None, {"mode": "deterministic", "reason": "providers_unreadable"}
|
||||
providers = cfg.get("providers", {})
|
||||
bindings = cfg.get("role_bindings", {})
|
||||
provider_id = os.environ.get("CASAN_CHAT_MODEL_PROVIDER") or bindings.get("codegen", "") or bindings.get("read_only", "")
|
||||
provider = providers.get(provider_id, {})
|
||||
model_spec = provider.get("model")
|
||||
if not model_spec:
|
||||
return None, {"mode": "deterministic", "reason": "provider_unresolved"}
|
||||
pclass = provider.get("class", "local")
|
||||
if pclass == "cloud" and provider.get("requires_key"):
|
||||
key_env = provider.get("key_env", "")
|
||||
if key_env and not os.environ.get(key_env):
|
||||
return None, {"mode": "deterministic", "reason": "cloud_key_unset"}
|
||||
router = os.environ.get("CASAN_CHAT_MODEL_ROUTER") or MODEL_ROUTER
|
||||
env = os.environ.copy()
|
||||
if pclass == "cloud":
|
||||
env["CASAN_PREFLIGHT"] = "1"
|
||||
prompt = "\n".join([
|
||||
"You are CASAN's governed codegen assistant. Produce a SMALL Python draft",
|
||||
"fulfilling the request. Output code only. No shell, no network, no file I/O.",
|
||||
f"REQUEST: {args.message[:400]}",
|
||||
])
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
pf = os.path.join(td, "p.txt")
|
||||
oj = os.path.join(td, "o.json")
|
||||
write_text(pf, prompt)
|
||||
r = subprocess.run(["bash", router, pf, oj, "--role", "generate", "--model", model_spec],
|
||||
cwd=ROOT, capture_output=True, text=True, env=env)
|
||||
if r.returncode != 0 or not os.path.isfile(oj):
|
||||
return None, {"mode": "deterministic", "reason": "model_unavailable"}
|
||||
try:
|
||||
out = json.load(open(oj, encoding="utf-8"))
|
||||
except Exception:
|
||||
return None, {"mode": "deterministic", "reason": "model_output_unreadable"}
|
||||
text = (out.get("text") or "").strip()
|
||||
if not text:
|
||||
return None, {"mode": "deterministic", "reason": "model_empty"}
|
||||
return text, {
|
||||
"mode": "model",
|
||||
"provider": provider_id,
|
||||
"model": model_spec,
|
||||
"input_tokens": int(out.get("input_tokens") or 0),
|
||||
"output_tokens": int(out.get("output_tokens") or 0),
|
||||
}
|
||||
|
||||
|
||||
def certify_codegen_draft(args, router, binding):
|
||||
@@ -370,7 +440,8 @@ def certify_codegen_draft(args, router, binding):
|
||||
d = codegen_dir(run_id)
|
||||
artifact_path = os.path.join(d, "draft.py")
|
||||
criteria_path = os.path.join(d, "success-criteria.json")
|
||||
draft = render_codegen_draft(args, binding)
|
||||
body, synth_meta = _model_codegen_body(args)
|
||||
draft = render_codegen_draft(args, binding, body, synth_meta)
|
||||
write_text(artifact_path, draft)
|
||||
write_json(criteria_path, {
|
||||
"must_contain": ["CODEGEN_DRAFT", "GENERATED_BY_CASAN_CHAT", "def generated_chat_draft"],
|
||||
@@ -389,6 +460,7 @@ def certify_codegen_draft(args, router, binding):
|
||||
"artifact": artifact_path,
|
||||
"success_criteria": criteria_path,
|
||||
"artifact_scan": {"ok": False, "output": scan_out},
|
||||
"synthesis": synth_meta,
|
||||
}, 2
|
||||
|
||||
loop_env = loop_env_for(args)
|
||||
@@ -428,6 +500,7 @@ def certify_codegen_draft(args, router, binding):
|
||||
"success_criteria": criteria_path,
|
||||
"artifact_scan": {"ok": True, "output": scan_out},
|
||||
"tool_output_scan": {"ok": output_scan_rc == 0, "output": output_scan_msg},
|
||||
"synthesis": synth_meta,
|
||||
"output": (r.stdout + r.stderr).strip(),
|
||||
"trace_verify": {"ok": trace_rc.returncode == 0, "output": (trace_rc.stdout + trace_rc.stderr).strip()},
|
||||
"replay": {"ok": replay_rc.returncode == 0, "output": (replay_rc.stdout + replay_rc.stderr).strip()},
|
||||
@@ -478,6 +551,7 @@ def finish_codegen(args, router, binding, loop_run, rc: int):
|
||||
"artifact": os.path.relpath(artifact, ROOT) if artifact and artifact.startswith(ROOT) else artifact,
|
||||
"artifact_scan": loop_run.get("artifact_scan"),
|
||||
"tool_output_scan": loop_run.get("tool_output_scan"),
|
||||
"synthesis": loop_run.get("synthesis"),
|
||||
},
|
||||
}, ensure_ascii=False))
|
||||
return rc
|
||||
@@ -626,6 +700,8 @@ def ask(args) -> int:
|
||||
if router.get("mode") == "CODEGEN":
|
||||
loop_run, loop_rc = certify_codegen_draft(args, router, binding)
|
||||
return finish_codegen(args, router, binding, loop_run, loop_rc)
|
||||
if getattr(args, "stream", False) and router.get("mode") in ("READ_ONLY", "ANALYSIS"):
|
||||
return run_and_passthrough(["python3", READONLY, "ask", "--stream", *common])
|
||||
return run_mode(["python3", READONLY, "ask", *common], binding)
|
||||
|
||||
|
||||
@@ -647,6 +723,7 @@ def main() -> int:
|
||||
askp.add_argument("--agent", default="")
|
||||
askp.add_argument("--skill", default="")
|
||||
askp.add_argument("--delegation-level", type=int, default=0)
|
||||
askp.add_argument("--stream", action="store_true")
|
||||
askp.set_defaults(func=ask)
|
||||
sub.add_parser("verify-audit").set_defaults(func=lambda _args: verify_audit())
|
||||
args = ap.parse_args()
|
||||
|
||||
Reference in New Issue
Block a user