feat: add governed chat agent selection

This commit is contained in:
thanhnv
2026-07-08 22:19:51 +09:00
parent 06d8d31b16
commit 004afa73c9
18 changed files with 677 additions and 39 deletions
@@ -24,6 +24,7 @@ def project_root() -> str:
ROOT = project_root()
BIN = os.path.join(ROOT, "packages", "casan-harness", "scripts", "bash")
ROUTER = os.path.join(BIN, "prompt-mode-router.py")
AGENT_RESOLVER = os.path.join(BIN, "chat-agent-resolver.py")
READONLY = os.path.join(BIN, "chat-readonly.py")
OPERATOR = os.path.join(BIN, "chat-operator.py")
@@ -36,13 +37,69 @@ def classify(message: str):
return {"mode": "BLOCK", "risk": "high", "reason": "router_invalid_json", "matched_rules": [r.stderr.strip()]}
def run_mode(args, binding):
r = subprocess.run(args, cwd=ROOT, capture_output=True, text=True)
try:
payload = json.loads(r.stdout)
payload["agent_binding"] = binding
print(json.dumps(payload, ensure_ascii=False))
except Exception:
if r.stdout:
sys.stdout.write(r.stdout)
if r.stderr:
sys.stderr.write(r.stderr)
return r.returncode
def run_and_passthrough(args):
r = subprocess.run(args, cwd=ROOT, text=True)
return r.returncode
def default_agent_for_mode(mode: str) -> str:
if mode == "OPERATOR":
return "ops-operator"
if mode == "CODEGEN":
return "codegen-draft"
return "evidence-reader"
def bind_agent(args, router):
agent = args.agent or default_agent_for_mode(router.get("mode", "READ_ONLY"))
tools = []
if router.get("mode") == "OPERATOR":
# The operator primitive resolves the exact registered action later; the
# agent bind still proves this turn is allowed to use the operator class.
tools.append("run-chat-tests")
cmd = [
"python3", AGENT_RESOLVER, "bind",
"--agent", agent,
"--actor", args.actor,
"--role", args.role,
"--project", args.project,
"--tenant", args.tenant,
"--delegation-level", str(args.delegation_level),
]
if args.skill:
cmd += ["--skill", args.skill]
for tool in tools:
cmd += ["--tool", tool]
r = subprocess.run(cmd, cwd=ROOT, capture_output=True, text=True)
if r.returncode != 0:
sys.stdout.write((r.stdout or r.stderr).strip() + "\n")
return r.returncode, None
try:
return 0, json.loads(r.stdout)
except Exception:
print(json.dumps({"success": False, "decision": "DENIED", "reason": "agent_resolver_invalid_json"}, ensure_ascii=False))
return 2, None
def ask(args) -> int:
router = classify(args.message)
bind_rc, binding = bind_agent(args, router)
if bind_rc != 0:
return bind_rc
common = [
"--message", args.message,
"--actor", args.actor,
@@ -51,8 +108,8 @@ def ask(args) -> int:
"--tenant", args.tenant,
]
if router.get("mode") == "OPERATOR":
return run_and_passthrough(["python3", OPERATOR, "run", *common])
return run_and_passthrough(["python3", READONLY, "ask", *common])
return run_mode(["python3", OPERATOR, "run", *common], binding)
return run_mode(["python3", READONLY, "ask", *common], binding)
def verify_audit() -> int:
@@ -65,9 +122,14 @@ def main() -> int:
askp = sub.add_parser("ask")
askp.add_argument("--message", required=True)
askp.add_argument("--actor", default="anonymous")
askp.add_argument("--role", default="viewer")
askp.add_argument("--project", default="default")
askp.add_argument("--chat-id", default="")
askp.add_argument("--turn-id", default="")
askp.add_argument("--tenant", default="default")
askp.add_argument("--agent", default="")
askp.add_argument("--skill", default="")
askp.add_argument("--delegation-level", type=int, default=0)
askp.set_defaults(func=ask)
sub.add_parser("verify-audit").set_defaults(func=lambda _args: verify_audit())
args = ap.parse_args()