Every agent framework eventually hits the same wall: the model is happy to rm -rf if that looks like progress. What separates a demo from something you can run near production code is not the reasoning loop — it is everything wrapped around it. Who is allowed to call which tool? With what arguments? And who gets asked when the answer is "it depends"?
That is what solon-ai-harness is about. It is the orchestration container at the center of Solon AI's agent stack, and this is a tour of how it answers those questions — mostly in code you can read in an afternoon, which is the point.
What a "harness" actually does
The word harness is borrowed from the gear that lets you safely ride something more powerful than you. HarnessEngine earns the name honestly: at construction time it wires together twenty-plus components from eight dependency packages — the terminal tools, skills, web search, LSP, memory, MCP and OpenAPI gateways — and at build time AgentFactory.create() hands each agent only the slice it is entitled to, based on its definition's tool list.
A harness is a capability integrator with a bouncer at every door.
The elegant part: an agent's definition is a Markdown file.
---
name: "bash"
description: "Bash 命令执行专家,专门执行 git 操作、命令行任务和终端操作(没有文件写权限)"
tools: ["list", "read", "bash"]
---
你是一个命令行执行专家,专门负责执行各种 shell 命令和操作...
YAML frontmatter, a system prompt, done. The tools list is the permission list — the factory resolves each name to a component and registers nothing else. The module ships with five built-in agents defined this way: general (all public tools), bash, explore, plan, and one hidden git-summary. Your own agents are just more Markdown, loaded from a mount:
HarnessEngine engine = HarnessEngine.of("/data/work/", ".tmp")
.toolsAdd(ToolName.TOOL_ALL_FULL)
.mountAdd(Mount.builder()
.alias("@global-agents")
.type(MountType.AGENTS)
.source(FileMountSource.of(Paths.get(
System.getProperty("user.home"), ".soloncode", "agents")))
.build())
.modelAdd(new ChatConfig().then(s -> {
s.setApiUrl("https://api.deepseek.com");
s.setModel("deepseek-v4-flash");
}))
.build();
Note the mount alias: agents can be swapped per environment (team server vs. laptop) without touching engine code. Definitions are data, not code.
The permission model: three verdicts, one tie-break rule
Under the hood, every interception question ends in one of three verdicts: ALLOW, DENY, or ASK (defer to a human). The PermissionEngine evaluates rules with a semantics you can state in one sentence:
Higher
prioritywins; among equal priorities, DENY beats ALLOW beats ASK; if nothing matches, fall back to the caller's default decision.
new PermissionEngine().evaluate(toolName, args, context);
// no rule matched → defaults to ASK
That default-to-ASK is a philosophical choice worth noticing: when the harness doesn't know, it asks a human rather than guessing.
Rules themselves are tiny objects — either a glob pattern or a custom BiPredicate on the raw arguments, never both:
PermissionRule.withPattern("bash", PermissionBehavior.ALLOW, "git status *", 10);
// or, with a matcher:
PermissionRule.of("bash", PermissionBehavior.DENY,
(tool, args) -> ((String) args.get("command")).contains("rm -rf"),
"Destructive command blocked.", 20);
(The factory split is deliberate: of(...) for matcher rules, withPattern(...) for glob rules — the two can't be mixed on one rule.)
And rules compose across scopes: the engine holds a global PermissionContext, and each agent can carry its own delta — at evaluation time the agent-level rules merge on top of the global ones. Team-wide "never touch /etc", plus one agent that may force-push in the release repo.
Three strategies, one interceptor
PermissionEngine is the generic brain, but the harness also ships three domain-specific strategies that plug into the HITL (human-in-the-loop) interceptor — one per dangerous tool family:
1. BashToolStrategy — the deep one
Bash is where agents do real damage, so this strategy layers its defense:
Built-in rules run first, at priority 100 (user rules can't out-prioritize them without trying). The built-in set, straight from the source:
| # | Rule | Verdict |
|---|---|---|
| 1 | Empty command | ALLOW (nothing to check) |
| 2 | Contains backtick, $(, or /dev/
|
DENY — injection / device redirection |
| 3 |
sudo, su, chmod, chown, passwd… |
DENY — privilege escalation |
| 4 | Contains ../
|
DENY — path traversal |
| 5 | Touches /etc/, /var/, /root/, ~/.ssh/, shell rc files |
DENY — sensitive system files |
| 6a | Whitelisted read-only command | ALLOW |
| 6b | Trailing `\ |
, &&, \ |
Then comes the part I find genuinely clever: the read-only whitelist. Fifty-plus commands — {% raw %}grep, ls, cat, head, wc, diff, jq, sed… plus two-word git read commands like git log and git diff — pass automatically. A pipeline passes only if every segment is read-only: cat app.log \| grep ERROR is fine, cat app.log \| sh is not.
Everything else defaults to ASK. Which means the interruption model is: boring commands run, weird commands ask, dangerous commands never run. As a developer, you only get pinged for the interesting cases.
One design note: BashToolStrategy's no-match default is ALLOW, not ASK — the built-in whitelist already classifies the known universe; unknown-but-not-flagged commands are trusted. You can flip either layer: internalRulesEnabled(false) drops the built-in rules entirely, or set a stricter defaultDecision.
2. WriteToolStrategy
The same shape, aimed at file writes: deny ../ traversal, deny writes to system paths and shell rc files. Path-based instead of content-based — shorter rule list, same two-rule skeleton.
3. WebToolStrategy
A hostname denylist: registered risky domains are rejected outright, and the rejection message dynamically includes the offending hostname so the model (and the human watching) sees why.
Three strategies, three shapes of risk: command text, file path, network destination. Adding your own for a new tool family is the same two steps: a couple of PermissionRules, and the PermissionContext supplier wiring.
Subagents, and the tools that make more agents
Two more pieces complete the picture:
-
TaskTalent— the subagent dispatcher: delegate work to a named agent (serial) or fan out several in parallel. Agents run with their own tool entitlement; thebashagent can't write files because its Markdown never asked for the write tool. -
GenerateTalent— an agent that creates other agents at runtime: describe a one-off role, get a definition, instantiate, run, discard. Combined withAgentFactory.createSubagent(definition)you can spin up narrowly-scoped workers on demand:
AgentDefinition def = new AgentDefinition();
def.setSystemPrompt("You audit generated SQL for full-table scans.");
def.getMetadata().addTools(ToolName.TOOL_READ); // read-only, by construction
ReActAgent auditor = engine.createSubagent(def).build();
An auditor that literally cannot write anything. Not "was told not to" — cannot.
Worth stealing even if you never use Solon
Strip away the Java and the design still transfers:
- Make the permission list the definition format. Frontmatter in a Markdown file is reviewable, diffable, and portable — permissions as data beats permissions as wiring.
- Read-only whitelists before denylists. A whitelist turns "ask for everything" from harassment into a signal.
- Three verdicts, not two. ASK is the pressure valve that keeps DENY rare and meaningful.
- Deterministic tie-breaks. DENY > ALLOW > ASK at equal priority means rule conflicts resolve the same way every time — no "whichever rule registered first" roulette.
- Built-in rules outrank user rules. The floor stays under your feet even when users get creative.
Solon AI runs Java 8 through 26, Apache 2.0, and the harness module is on Maven Central as org.noear:solon-ai-harness (in the 4.1 line; the module itself has been published since the 3.10 series). Repository: github.com/opensolon/solon-ai.
Every technical claim in this piece was checked against the solon-ai-harness source (v4.1 line) — rule lists, priorities, defaults, and agent definitions all read directly from the repository. If you find a discrepancy, the comments are the fastest way to reach me.
Top comments (0)