From 428b3565b257cd2c8fc969932d167f01f5c7aca5 Mon Sep 17 00:00:00 2001 From: Kral Date: Sat, 3 Oct 2026 14:29:37 +0200 Subject: [PATCH] Proxy: ADT REST write+activate fallback when EPOD does not activate a second PROG/FUNC write Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014aUaQeLnwbb1zTpN7kHeat --- harness/adt_client.py | 28 ++++++++++++++++++++++++++++ harness/generator.py | 2 +- harness/proxy.py | 30 ++++++++++++++++++++++++++++++ harness/runner.py | 2 ++ 4 files changed, 61 insertions(+), 1 deletion(-) diff --git a/harness/adt_client.py b/harness/adt_client.py index 8be4c13..aba87ca 100644 --- a/harness/adt_client.py +++ b/harness/adt_client.py @@ -70,3 +70,31 @@ class AdtClient: out[uri.group(1) if uri else "?"] = (ok is not None and ok.group(1) == "true", msg.group(1) if msg else "") return out + + def write_activate(self, uri, name, source): + """Lock, write source/main, unlock, activate. Fallback for EPOD: a second sap_push_source on a + PROG or FUNC writes an inactive version that is not in the inactive list, so no activation runs + (probe Z0ZZZ999_PROBE_PROG, 2026-10-03). Returns (activated, [(severity, text)]).""" + if not self.csrf: + self.fetch_csrf() + st = {"X-sap-adt-sessiontype": "stateful"} + _, _, t = self._req("POST", uri + "?_action=LOCK&accessMode=MODIFY", None, + dict(st, Accept="application/vnd.sap.as+xml;charset=UTF-8;" + "dataname=com.sap.adt.lock.result")) + lh = re.search(r"(.*?)", t).group(1) + try: + self._req("PUT", f"{uri}/source/main?lockHandle={lh}", source, + dict(st, **{"Content-Type": "text/plain; charset=utf-8"})) + finally: + self._req("POST", f"{uri}?_action=UNLOCK&lockHandle={lh}", None, st) + body = ('' + f'' + '') + _, _, t = self._req("POST", "/sap/bc/adt/activation?method=activate&preauditRequested=true", body, + {"Content-Type": "application/xml", "Accept": "application/xml"}) + msgs = [(m.group(1), re.sub(r"<[^>]+>", "", m.group(2)).strip()) + for m in re.finditer(r']*\btype="(\w)"[^>]*>(.*?)', t, re.S)] + done = 'activationExecuted="true"' in t and not any(sev in ("E", "A") for sev, _ in msgs) + return done, msgs + diff --git a/harness/generator.py b/harness/generator.py index 6e8b4e8..c3f30e1 100644 --- a/harness/generator.py +++ b/harness/generator.py @@ -495,7 +495,7 @@ def make_k_variant(src_id, new_id, style, model, base_url, run_base, tool_schema src_dir, task_dir = os.path.join(pool_root, src_id), os.path.join(pool_root, new_id) b = bundle_of(src_dir) b["files"] = {k: v for k, v in b["files"].items() - if not (k.startswith("faulty/") or k in ("mutation.json",))} + if not (k.startswith("faulty/") or k in ("mutation.json", "review.json", "empirical.json"))} spec = b["files"]["spec.md"] terms = contract_terms(b["task"]) messages = [{"role": "user", "content": diff --git a/harness/proxy.py b/harness/proxy.py index 6ce50e5..156323e 100644 --- a/harness/proxy.py +++ b/harness/proxy.py @@ -50,6 +50,8 @@ class ToolProxy: self.fail_streak = 0 self.max_fail_streak = 0 self.log = open(log_path, "a") + self.adt = None + self.fallbacks = [] # writes that needed the ADT activation fallback def schemas(self): tools = [t for t in self.mcp.list_tools() if t["name"] in MODEL_TOOLS] @@ -123,6 +125,8 @@ class ToolProxy: if tool in WRITE_TOOLS and (tool == "sap_activate" or args.get("activate", True)): self.activations += 1 err, text = self.mcp.call(tool, args) + if tool == "sap_push_source": + err, text = self._activation_fallback(args, err, text) result = (err, self._filter(tool, text)) if tool in WRITE_TOOLS: failed = err or '"success":false' in text.replace(" ", "") @@ -133,6 +137,32 @@ class ToolProxy: self.log.flush() return result + def _activation_fallback(self, args, err, text): + """EPOD does not activate a second write of a PROG or FUNC (outcome notExecuted, and sap_activate + cannot help). Write and activate the same source through ADT REST; the agent sees one normal result.""" + otype = str(args.get("objectType", "")).upper() + if otype not in ("PROG", "FUNC") or args.get("includeType") or args.get("activate") is False \ + or '"outcome":"notExecuted"' not in text.replace(" ", ""): + return err, text + name = str(args.get("objectName", "")).lower() + uri = (f"/sap/bc/adt/programs/programs/{name}" if otype == "PROG" else + f"/sap/bc/adt/functions/groups/{str(args.get('functionGroup', '')).lower()}/fmodules/{name}") + try: + if self.adt is None: + from .adt_client import AdtClient + self.adt = AdtClient() + done, msgs = self.adt.write_activate(uri, name.upper(), args.get("source", "")) + except Exception as e: # noqa: BLE001 keep the EPOD result when the fallback fails + self.fallbacks.append({"object": name, "error": str(e)[:200]}) + return err, text + self.fallbacks.append({"object": name, "activated": done}) + out = {"success": done, + "message": "Source written and activation requested." if done else + "Source written. Activation failed. See the activation messages.", + "activation": {"messages": [{"severity": sv, "message": tx} for sv, tx in msgs], + "activationExecuted": done, "success": done}} + return False, json.dumps(out) + def note(self, kind, content): self.log.write(json.dumps({"t": time.time(), "kind": kind, "content": content}) + "\n") self.log.flush() diff --git a/harness/runner.py b/harness/runner.py index 40341f1..f5ce9b6 100644 --- a/harness/runner.py +++ b/harness/runner.py @@ -207,6 +207,8 @@ class Runner: rep["agent_seconds"] = round(time.time() - t1, 1) rep["tool_calls"], rep["activations"] = proxy.calls, proxy.activations rep["max_fail_streak"] = proxy.max_fail_streak + if proxy.fallbacks: + rep["adt_fallbacks"] = proxy.fallbacks # 3 collect hidden_names = {o["name"].upper() for o in task.objects("hidden_tests")}