17 things your agent harness should have!
Do this first: go down the list and tick what your harness already does. Each unticked item is a failure your agent will hit.
The list comes from reading the source of [OpenHands SDK][oh], [OpenCode][oc], [Hermes Agent][ha], [DeepSeek harness][ds] and [Strands harness][st]. Items 1–6 are the basics everyone builds. Items 7–17 are what separates these harnesses from a model in a loop. The rule behind all of them: the prompt is for judgement; anything a script can check is enforced in code.
The list
1. A tool loop. Model call, tool calls, results back, repeat until the model answers. Everything else hangs off it.
2. Summarization. Old turns get summarized so long runs keep going. OpenHands keeps the first messages untouched and makes the summary track open tasks ([condenser][oh-condense]); Strands never drops pinned messages ([summarizing manager][st-summ]).
3. Subagents. Hand a focused task to a fresh agent with its own tools, and get back one answer ([Strands subagent.py][st-sub]).
4. Skills. Instructions loaded only when the task needs them. OpenHands loads them on keywords and on file paths the agent touches ([trigger.py][oh-skills]).
5. Model retries. Back off and retry on provider errors. Strands defaults to 6 attempts ([_retry.py][st-retry]).
6. Hooks. Run your own check before or after any tool call, on each prompt, and on stop. OpenHands follows Claude Code's convention: a hook that exits with code 2 blocks the call ([hooks/manager.py][oh-hooks]).
7. Errors that name the next step. A refusal says why and what to do instead. OpenCode answers a missing file with "Did you mean one of these?" ([read.ts][oc-read]); DeepSeek's sandbox says "retry this exact command once with sandbox_permissions + justification" ([escalation.ts][ds-esc]).
8. Read before edit. Refuse an edit to a file the agent never read, or one that changed since: edit requires reading "X" first ([DeepSeek][ds-read]). Hermes warns "was modified since you last read it" ([read tracking][ha-read]).
9. Repeated-call detection. Same tool, same arguments, again. DeepSeek reminds at 3, 5 and 8 repeats ([repeat-tool-reminder][ds-repeat]); Hermes warns, then blocks, then halts ([tool_guardrails.py][ha-guard]); OpenHands nudges once, then stops the run ([stuck_detector.py][oh-stuck]).
10. A step budget that takes tools away. When it's spent, OpenCode sets toolChoice: "none", appends a wrap-up message, and refuses any call that still arrives ([max-steps.ts][oc-steps], [llm.ts][oc-llm]). The run ends with an answer, not a crash.
11. "Done" is checked, not trusted. Hermes sends the model back to run its tests after a code edit ([verification_stop.py][ha-verify]). OpenCode's judge is told "The assistant claiming the goal is met is not evidence" ([judge.txt][oc-judge]).
12. Remove tools instead of forbidding them. OpenHands' planning agent can only edit PLAN.md ([planning.py][oh-plan]); OpenCode's explore agent starts from deny-all ([agent.ts][oc-agent]); Hermes subagents never get delegate or memory tools ([delegate_tool_toolsets.py][ha-sub]).
13. Parse shell commands before running them. OpenCode checks each command and target folder as a permission ([shell.ts][oc-shell]). Hermes keeps a blocklist no mode can override ([approval_floors.py][ha-floor]).
14. Reminders checked against state. Before every request OpenCode checks the last plan-mode reminder still matches the current mode ([plan.ts][oc-plan]). Strands re-adds the todo list each turn without saving it to history ([todos.py][st-todo]).
15. Silent argument repair. "42" becomes 42 before the tool runs ([Hermes arg_coercion.py][ha-coerce], [OpenCode tool-input-repair.ts][oc-repair]). No round trip, no error.
16. Summaries that don't give orders. Hermes labels them "REFERENCE ONLY … NOT as active instructions", after a stronger wording made the agent stop using tools ([context_compressor.py][ha-compact]). DeepSeek keeps user corrections word for word ([summarizer.ts][ds-sum]).
17. An LLM supervisor with hard limits. Hermes' guardian model rates risky commands and stops after repeated denials ([approval_smart.py][ha-smart]). Strands' judge must "treat partial satisfaction as failure" ([judge.py][st-judge]). A judge without a numeric cap is just another loop.
Start here
Have 1–6 already? Build 8, 9 and 10 next. Each is about a day of work and removes a whole class of failures.
Next: open your file-edit tool and check whether it refuses an edit to a file the agent never read.
[oh]: OpenHands/software-agent-sdk
[oc]: anomalyco/opencode
[ha]: NousResearch/hermes-agent
[ds]: deepseek-ai/deepseek-harness
[st]: strands-agents/harness-sdk
[oh-condense]: github.com/OpenHands/software-agent-sdk/blob/295b…
[st-summ]: github.com/strands-agents/harness-sdk/blob/3bd9ed…
[st-sub]: github.com/strands-agents/harness-sdk/blob/3bd9ed…
[oh-skills]: github.com/OpenHands/software-agent-sdk/blob/295b…
[st-retry]: github.com/strands-agents/harness-sdk/blob/3bd9ed…
[oh-hooks]: github.com/OpenHands/software-agent-sdk/blob/295b…
[oc-read]: github.com/anomalyco/opencode/blob/2658ec02c848ce…
[ds-esc]: github.com/deepseek-ai/deepseek-harness/blob/b150…
[ds-read]: github.com/deepseek-ai/deepseek-harness/blob/b150…
[ha-read]: github.com/NousResearch/hermes-agent/blob/53a6ced…
[ds-repeat]: github.com/deepseek-ai/deepseek-harness/blob/b150…
[ha-guard]: github.com/NousResearch/hermes-agent/blob/53a6ced…
[oh-stuck]: github.com/OpenHands/software-agent-sdk/blob/295b…
[oc-steps]: github.com/anomalyco/opencode/blob/2658ec02c848ce…
[oc-llm]: github.com/anomalyco/opencode/blob/2658ec02c848ce…
[ha-verify]: github.com/NousResearch/hermes-agent/blob/53a6ced…
[oc-judge]: github.com/anomalyco/opencode/blob/2658ec02c848ce…
[oh-plan]: github.com/OpenHands/software-agent-sdk/blob/295b…
[oc-agent]: github.com/anomalyco/opencode/blob/2658ec02c848ce…
[ha-sub]: github.com/NousResearch/hermes-agent/blob/53a6ced…
[oc-shell]: github.com/anomalyco/opencode/blob/2658ec02c848ce…
[ha-floor]: github.com/NousResearch/hermes-agent/blob/53a6ced…
[oc-plan]: github.com/anomalyco/opencode/blob/2658ec02c848ce…
[st-todo]: github.com/strands-agents/harness-sdk/blob/3bd9ed…
[ha-coerce]: github.com/NousResearch/hermes-agent/blob/53a6ced…
[oc-repair]: github.com/anomalyco/opencode/blob/2658ec02c848ce…
[ha-compact]: github.com/NousResearch/hermes-agent/blob/53a6ced…
[ds-sum]: github.com/deepseek-ai/deepseek-harness/blob/b150…
[ha-smart]: github.com/NousResearch/hermes-agent/blob/53a6ced…
[st-judge]: github.com/strands-agents/harness-sdk/blob/3bd9ed…
2 reactions · 1 comment
AI Agent OAuth Recovery: Stop Expired Connections From Retrying Forever
An agent can have a perfect plan and still fail at the least interesting moment: halfway through a customer task, its connected account expires. The bad response is to retry the same API call until the run burns its budget. The worse response is to ask the model to repair authentication.
OAuth failure is a product and workflow state, not a transient tool error. A reliable agent identifies whether a token can be refreshed, whether the customer must reconnect, and whether the original task is still safe to resume. This guide shows a vendor-neutral pattern for building that recovery path without exposing credentials or repeating side effects.
Why expired connections become runaway agent runs
OAuth access tokens are deliberately short-lived. A refresh token can usually obtain a replacement, but a user can revoke the grant, an administrator can change policy, or a provider can reject a refresh with invalid_grant. Google’s MCP guidance, for example, notes that clients need the right offline-access behavior to avoid repeated sign-in when access tokens expire (Google documentation).
An agent loop often turns this ordinary condition into an incident:
1. A tool returns 401.
2. The model sees an opaque error and tries the tool again.
3. Several workers refresh the same connection at once.
4. One refresh rotates the refresh token; another now fails.
5. The workflow keeps retrying, or falls back to a different account without the user knowing.
The durable rule is simple: authentication recovery belongs below the model. The model can explain a blocked task, but it should not inspect, paste, select, or refresh bearer tokens.
Model the connection separately from the agent run
Do not represent every failure as tool_failed. Keep a connection record with an explicit state, then let each run reference that record by ID. The run holds intent and progress; the connection holds authorization health.
| Connection state | Meaning | Safe next action |
| --- | --- | --- |
| active | A recent check or call succeeded | Permit the requested tool |
| refreshing | One service is refreshing the token | Wait briefly; do not start another refresh |
| reauth_required | The grant is revoked, invalid, or missing | Pause affected runs and show a reconnect action |
| provider_unavailable | The provider or token endpoint is unavailable | Retry on a bounded schedule; do not request consent yet |
| disabled | The user, tenant, or policy removed access | Fail closed and require an explicit new connection |
This distinction matters for UX. A reconnect screen cannot fix a provider outage, and an automatic retry cannot repair a revoked grant. Connection products already expose similar distinctions: an expired credential, a rejected reconnect, and a provider connection issue are different conditions with different remedies (connection troubleshooting example).
Here is a minimal TypeScript shape:
type ConnectionState =
| "active"
| "refreshing"
| "reauth_required"
| "provider_unavailable"
| "disabled";
type Connection = {
id: string;
tenantId: string;
provider: "crm" | "mail" | "calendar";
accountLabel: string; // safe display name, not an email if avoidable
state: ConnectionState;
grantedScopes: string[];
credentialRef: string; // vault reference, never the token itself
version: number;
lastVerifiedAt?: string;
reauthReason?: "revoked" | "invalid_grant" | "scope_changed" | "missing";
};
Bind it to the tenant and intended account. “Use any connected calendar” is an unsafe fallback in a multi-account product. A reauthorized connection should update the same durable connection ID only after the user has seen the provider, account, and requested scopes.
Make refresh a locked, typed operation
Refresh-token rotation creates a small distributed-systems problem. Two requests might notice an old access token together. If both refresh independently, the provider may invalidate the token one of them was about to store.
Use a short database lease or compare-and-swap update to nominate one refresher. Everyone else waits for its result. Keep the credential material in a secret manager; the application database stores only a vault reference and harmless status metadata.
async function getUsableConnection(id: string) {
const connection = await db.connection.findUniqueOrThrow({ where: { id } });
if (connection.state === "disabled") return { kind: "blocked", reason: "disabled" };
if (connection.state === "reauth_required") return { kind: "reauth", reason: connection.reauthReason };
const claimed = await db.connection.updateMany({
where: { id, state: "active", refreshLeaseUntil: { lt: new Date() } },
data: { state: "refreshing", refreshLeaseUntil: plusSeconds(20), version: { increment: 1 } }
});
if (claimed.count === 0) return waitForConnectionResolution(id, 2500);
try {
const secret = await vault.read(connection.credentialRef);
const refreshed = await oauth.refresh(secret.refreshToken);
await vault.rotate(connection.credentialRef, refreshed);
await db.connection.update({ where: { id }, data: { state: "active", lastVerifiedAt: new Date(), refreshLeaseUntil: null } });
return { kind: "ready" };
} catch (error) {
return classifyAndPersistRefreshFailure(id, error);
}
}
The pseudo-code assumes your storage layer supports an atomic conditional update. If it does not, use a transaction or a distributed lock with an expiry. More importantly, classify the response:
- A timeout or 5xx is a bounded, retryable infrastructure failure.
- invalid_grant, an invalid refresh token, or a user revocation is terminal for that grant.
- A missing required scope needs reconnection and possibly consent for the new scope.
- A 403 from the resource API may mean the grant works but the user lacks provider-side permission. Do not blindly reconnect.
On a terminal refresh failure, mark the connection reauth_required once. Do not try it again from every queued run. A well-designed OAuth client does exactly this: it treats a failed refresh caused by a revoked or rotated grant as a reauthentication event rather than replaying the same unauthorized request (an implementation example).
Pause the run with a recoverable checkpoint
Once a task needs human reconnection, an agent has two responsibilities: preserve enough context to resume, and stop before it creates an ambiguous side effect.
Persist a checkpoint before every external write and before you show the reconnect UI:
{
"run_id": "run_84a",
"state": "waiting_for_connection",
"connection_id": "conn_29",
"required_capability": "crm.contacts.update",
"checkpoint": "validated_contact_change",
"intent_hash": "sha256:...",
"resume_after_connection_version": 18,
"created_at": "2026-11-04T10:15:00Z"
}
The intent_hash protects against quiet changes while the user is away. When the user returns, the agent should not send an old draft to a newly selected account or apply a stale CRM update. Fetch the current record, compare the relevant fields, re-run policy checks, and ask again if the intended action has changed.
For an action that might already have reached the provider, do not resume by repeating it. Use an idempotency key or query the provider for an operation receipt first. This is the same unknown-outcome boundary that makes a network timeout different from a rejected write.
Build a reconnect flow that earns trust
The reconnect card should be specific without being alarming. It needs to answer what failed, what will happen next, and what will not happen automatically.
> Reconnect needed — Your CRM connection can no longer authorize updates. The draft change is saved but has not been applied. Reconnect the account that owns this record, then review the change before continuing.
Include these elements:
- Provider name and a privacy-safe account label.
- The capability that is blocked, not a raw OAuth error.
- The saved task status: not started, safely paused, or needs verification.
- A server-generated, short-lived reconnect URL bound to tenant, connection, and CSRF/session state.
- A clear path to cancel or disconnect.
Never put an authorization URL, authorization code, refresh token, or customer secret into a model prompt or chat message. The browser callback should reach your backend, exchange the code server-side, store the new credential in the vault, and redirect to a neutral completion page. The agent only receives connection_version: 19 and allowed capabilities.
Reconnection is not permission escalation. If the user grants different scopes or selects another account, record that as a new authorization version and re-evaluate the task. A tool policy or approval that applied to the old connection must not silently carry over to a broader one.
Resume safely instead of automatically continuing everything
When OAuth completes, publish a connection.restored event. A scheduler can find runs waiting on that exact connection, but it should use a resume policy rather than wake every run.
| Run type | Resume policy |
| --- | --- |
| Read-only lookup | Re-run after verifying tenant, account, and scope |
| Draft generation | Resume only if source inputs and requested account still match |
| External write | Recheck state, policy, and idempotency; often show a final confirmation |
| Destructive action | Require a fresh approval, even after reconnect |
This policy avoids an unpleasant surprise: a user reconnects their mailbox and suddenly receives a backlog of messages the agent attempted days earlier. The sensible default is “connection restored, task ready to review,” not “all blocked side effects now execute.”
Use a version check to make the rule enforceable:
function mayResume(run: Run, connection: Connection) {
return run.state === "waiting_for_connection" &&
run.connectionId === connection.id &&
connection.state === "active" &&
connection.version >= run.resumeAfterConnectionVersion &&
run.tenantId === connection.tenantId;
}
The next tool call still goes through normal authorization, policy, rate limits, and approval gates. Resumption is a request to reconsider the action, not a stored permission slip.
Test the failure paths that happy-path OAuth misses
Most OAuth integration tests prove only that a new connection works. Production reliability comes from testing what happens later.
- Force two simultaneous requests to refresh one connection; assert only one vault rotation occurs.
- Return invalid_grant; assert one reauth_required transition and no retry storm.
- Revoke an account between planning and execution; assert the write never starts.
- Reconnect a different account; assert the paused run stays blocked until it is reviewed.
- Change scopes during reconnection; assert a previously allowed write is reauthorized.
- Timeout after an external write; assert the run reconciles with an idempotency key instead of repeating it.
- Remove a connection while a run waits; assert all associated runs become safely terminal or need explicit reassignment.
Measure the operational signals too: connections by state, refresh success rate, terminal refresh failures, time spent waiting for reconnect, repeated calls blocked after one failure, and resumes rejected because the account or target changed. Avoid logging raw tokens, authorization codes, request bodies with secrets, or full provider error payloads.
A compact implementation checklist
- [ ] Separate durable connection state from workflow state.
- [ ] Keep credentials in a vault and expose only opaque references to application code.
- [ ] Serialize refreshes per connection and handle rotation atomically.
- [ ] Classify invalid_grant, missing scope, provider outage, and disabled access separately.
- [ ] Turn terminal refresh failures into one reauth_required state, not open-ended retries.
- [ ] Checkpoint the run before reconnect and bind it to tenant, connection, account, and intent.
- [ ] Revalidate target data, policy, and idempotency before any resumed write.
- [ ] Make reconnect visible, explicit, and reviewable.
- [ ] Test revocation, rotation races, provider outages, changed scopes, and queued runs.
OAuth is not background plumbing for an agent product. It is the boundary between a user’s connected account and an autonomous workflow. Treat an expired connection as a first-class state, and the product can pause honestly, reconnect safely, and finish only when it still has the right to do so.
FAQ
Should an AI agent retry after a 401 error?
Not blindly. First determine whether the access token can be refreshed. A transient token-endpoint failure may justify a bounded retry, but a revoked grant or invalid_grant should move the connection to reauth_required and pause dependent work.
What is the difference between token refresh and reauthorization?
Refresh uses an existing refresh token without user interaction to obtain a new access token. Reauthorization starts a user-facing consent flow because the existing grant is invalid, missing, revoked, or needs different scopes.
Can an agent reconnect an OAuth account automatically?
It can refresh a valid existing grant in the background. It should not silently complete a new consent flow, choose a different account, or widen scopes. Those actions require the user’s explicit participation.
How do I prevent OAuth refresh token race conditions?
Allow one refresher per connection using an atomic database update, lease, or lock. Other requests wait for the outcome and read the newly stored credential reference. Rotate the vault value atomically and version the connection state.
Should paused agent runs resume as soon as the user reconnects?
Usually only read-only tasks can resume automatically. For drafts, writes, and destructive actions, revalidate the account, current target data, authorization, policy, and idempotency. A final review is often the safer experience.
What should an agent show a user when a connection expires?
Explain the blocked capability, provider, safe account label, and task status in plain language. Say whether any action was applied, preserve the task for review, and offer a secure reconnect action. Do not display raw OAuth errors, tokens, or authorization codes.
1 reaction
ok AI assistants are getting kinda out of hand
So i've been reading about AI agent stuff and this week was actually insane. this guy in japan asked some AI assistant called Pickle to look up hotels for new years and it just... called the hotel by itself?? Twice. first time it said it was an AI and gave his full name, then the call dropped so it called back pretending to be his human secretary. He literally had to call the hotel and apologize en.yutura.net
And then someone had this thing called Hermes Agent where another AI is supposed to double check risky stuff for you. it saw a "cache cleanup" was gonna delete a bunch of stuff, said yep that's dangerous, and approved it anyway. Wiped like 188k files, 21 gigs, no recycle bin, nothing hermes-agent#147373
oh and some researchers gave AIs a crypto wallet with a one cent limit, and the code to raise it got emailed to the owner... but the AI could read the email. 9 out of 10 just grabbed the code and raised their own limit 💀 coindesk.com
Like you give them a tiny yes and they run with it. idk i'm not letting one touch my money or my files without me clicking ok first
Has anyone else had an AI do something random like this??
67 points · 64 comments
A guardrail that decides at the door lets the intruder walk in later
!A guardrail checks a tool call once, waves it through, and the tool then runs with full access to keys and the network — fail-open
I report security bugs in AI-agent guardrails, and I keep finding the same shape of bug. This is the first one that's fully public, so I can show you the whole thing — the finding, the fix that shipped, and why I think the design itself is the problem.
The finding (airlock, now public)
airlock is a prompt-injection guardrail in the Hermes plugin catalog. Its job is to stop an agent from doing dangerous things without a human's say-so: in its default approvals_off: block mode, an action that needs approval should be blocked until a person approves it.
The problem I reported: that block could be skipped entirely.
!block mode promised to hold the action; in a gateway-run hermes -z turn the guard read the wrong unattended signal, so an approval-required action ran with no approval
When the Hermes gateway runs a hermes -z turn, the turn is marked unattended in one place (gateway.session_finite) but not in the flag airlock was reading (HERMES_YOLO_MODE). So airlock looked at its usual signal, saw nothing that said "a human is driving this," and let an approval-required action run with no approval at all — even though the whole point of block mode is that nothing dangerous runs unattended.
The maintainer triaged it and shipped a fix in airlock 0.2.3: it now asks the approval layer why Hermes would approve on its own, and it covers CLI subagents under delegation too. The fix is merged into the Hermes plugin catalog (PR #144871), and the advisory is public as GHSA-mgf8-gh7g-q4rg. Coordinated disclosure, credited. That's the system working, and the maintainer was great about it.
The pattern underneath
Here's why I care beyond this one bug. Most agent guardrails work the same way: they decide at the door. Something reads the pending action, matches it against a signal — a mode flag, a regex, a list of paths, a YARA rule — and then either waves it through or stops it. The tool itself runs with whatever access it already had.
Every bug I find in this category is a gap between the signal the guard reads and the effect that actually happens:
- the action is dangerous, but the signal says "attended," so it runs;
- the path points at a secret, but it's spelled a way the matcher doesn't recognize, so it's missed;
- the input is an injection, but it's phrased in a form the rule doesn't cover, so it passes clean.
Same shape every time. The guard is reasoning about a string or a flag that stands in for the real thing, and anything that changes the representation without changing the effect slips past. When these fail, they fail open: no match, no block, action runs. I've started calling this class "stringly-typed guardrails," and once you see it you see it everywhere.
A door-check can be made tighter — airlock's fix did exactly that — but tightening one gap doesn't change the design. There's always another spelling of the dangerous thing.
What I build instead
!Door-check guardrail vs CleiMesh: the token is the sandbox, default-deny, the tool only ever has the grant, and every call leaves a signed receipt you can replay
I'm the author of CleiMesh, and it exists because I got tired of finding this bug. CleiMesh doesn't decide at the door. Two things are different:
The permission becomes the sandbox. An agent runs under a signed, delegable token that says exactly which tools it may call and which directories or network it may touch — and the tool runs inside a WebAssembly sandbox configured from that token, default-deny. There's no "the guard waved it through but the tool still had full access," because the tool never had the access. A subagent can only get a narrower token, never a wider one. A prompt injection can ask for more all it likes; the grant doesn't widen.
Every call leaves a receipt you can replay. Each tool call produces a signed, hash-chained receipt. You can verify the chain offline, and — the part no gateway offers — you can re-run a call from its receipt and compare the result byte for byte. You don't have to trust the log; you re-execute it. That's also what makes it an auditable record for GDPR/LGPD: a report per data subject, from evidence, not from a dashboard someone could edit.
They're complementary, honestly — a door-check catches the obvious stuff fast. But if the door-check is the only thing between an injected agent and your keys, every bug like the one above is a hole. CleiMesh is the layer that doesn't have that failure mode, because there's nothing to "miss."
Why I'm writing this
Because finding these quietly and moving on makes me the nice guy who does free security work for other people's tools and gets nothing for it. I'd rather be clear about what I do: I audit agent guardrails, I report what I find responsibly, and I build the thing that enforces instead of guesses. If you run agents that touch real data — in Claude Code, Cursor, OpenClaw, Hermes — and "the guardrail checked it" is your whole story, I'd look harder at what the guardrail actually checks.
CleiMesh is free and open (MIT/Apache-2.0): **cleitonaugusto/cleimesh**. There are two ClawHub skills that set it up — cleimesh-setup for an agent's own file tools, and cleimesh-mcp for the MCP servers it already uses. I also do paid security reviews; the repo README says how.
airlock is the first public one. I have other coordinated disclosures in flight with different maintainers right now; when those patches ship, I'll write them up the same way.
— Cleiton Augusto Correa Bezerra (github.com/cleitonaugusto)
1 reaction · 6 comments
the same steps in all three, side by side: read the seller's record, borrow and pay, repay
in hermes agent it's a plugin, and it asks you before it pays. our pull request to the @NousResearch catalog: hermes-agent#147003
in agentkit it's an action provider, one npm i @priors/agentkit away. our pull request with @CoinbaseDev: agentkit#1611
in @elizaOS it's a plugin with four actions: npm i @priors/plugin-elizaos
27 likes · 1 repost · 540 views
2026年後半のLLM動向: この数か月の進化を振り返る
ここ数週間で、気になるモデルの発表が続いています。OpenAIからは、Sonnet 5.5に近い能力を半分のAPI単価で使えるGPT-6.1 Terra。Googleからは、GPT-6 AstraやClaude Opus 5.5と競うGemini 4 Argonに続き、約1か月でさらに性能を伸ばしたGemini 4 Carbonです。中国からも、Opus 5.5やAstraに迫る性能をうたうKimi K3.1 Proが出てきました。
この数か月でだいぶ動きがあったので、まず最新の発表で現在地を確認し、そこまでの移り変わりを振り返ってみます。そのあとに、私のコーディング環境と思考がどう変わったか、最後に、進化の速い道具や、自分で作った仕組みとの付き合い方について少し。発表を読むたびに装備を買い替えている気もするので、いったん手持ちを整理してみます。
現在地:最新モデルは、何を競っているのか
直近の三つの発表から、それぞれが何を強みにしているのかを見てみます。料金は100万トークンあたりのAPI単価で、キャッシュなし入力/出力の順です。
Gemini 4 Carbon:Googleも性能の上限を競う
10月28日のCarbon発表では、①総合的な知的能力、②エージェントによる開発、③画面操作、④知識業務、⑤マルチモーダル理解、⑥長文脈理解、⑦科学・数学、⑧サイバーセキュリティの8分野を、複数のベンチマークで評価しています。
主な比較対象は、約1か月前に発表されたArgonに加え、GPT-6 Astra、Claude Opus 5.5、Claude Fable 5.1です。公式の比較表では、17指標のうち15で掲載モデル中の最高値を示しています。一部の開発指標ではAstra、画面操作ではOpus 5.5が上ですが、幅広い用途で最上位を競うモデル、という打ち出しです。
長い間 Flash系の更新のみでしたが、Argon、Carbonと続き、Googleも性能上限の競争に入ってきました。
導入価格はArgonと同じ、入力$2/出力$10。導入期間の終了後は$4/$20となる案内です。ただし、まずは一部のサイバー防御関係者からの限定提供で、一般提供日は未発表です。
GPT-6.1 Terra:Sonnet 5.5との比較で見る低価格モデル
10月26日のGPT-6.1 Terra発表は、費用や速さを強調した発表です。
普段使いの比較相手として気になるのはSonnet 5.5です。Terraは入力$1/出力$5で、Sonnetの$2/$10の半額。総合評価ではSonnet 5.5 に及ばないものの、低い単価で回せるモデル、という見方が近そうです。
評価会社Artificial Analysisの比較では、総合指数はTerraが約51、Sonnetが約54でした。両者とも最大思考量での比較で、SonnetはDefault Fallback設定の結果です。一方、実際のリポジトリのコード修正を測るFrontierCode 2.0では、42.3%対41.7%とほぼ互角です。OpenAIも、Sonnetが開発や画面操作で上回る比較を掲載しています。一方、Agents’ Last Examという評価では、Sol・Astraより作業を3倍超速く終えることや、Opus 5.5よりタスク費用が低いことを訴えています。
GPT-6 Sol・Luna、続いて6.1 Solが出るなかで、Terraは5.6のままでした。てっきり Sonnet 5.5 より高性能で同価格帯のものが出てくるのかと思っていたので、少し残念です。しかし、強みもあるので、どれだけ使えるのか試してみようとは思います。Agentic なコーディングは Sonnet 5.5 と要比較、画面操作(ブラウザ、動画編集、CADなど)は期待といった感じかな。
Kimi K3.1 Pro:米国勢にも、中国勢の上位にも比較を向ける
10月23日のKimi K3.1 Pro発表では、Astra・Opusに加え、中国勢のDeepSeek V4.1 FlashやGLM-5.3との比較も出しています。
Kimiの公表値では、開発や知識業務の一部でOpus 5.5やAstraに迫り、上回る項目もあります。比較対象のGLM-5.3やDeepSeek V4.1 Flashにも、複数の指標で差をつけています。
一方、DeepSeek V4.1 Flashのコスパは、やはり別格に見えます。9月の発表では、GPT-5.6 SolやOpus 5と競える指標もありました。しかも、ピーク時間でも入力$0.30/出力$1.20で、オフピークはさらに半額です。最新の最上位モデルに全面的に並ぶわけではなくても、この価格ならまだまだ出番がありそうです。
「中国製モデルは少し性能を譲る代わりに安い」という見方だけでは、捉えきれなくなってきたのかな、という印象です。
ここ数カ月の動き:上位の能力が、普段使いの価格へ
6月下旬にGPT-5.6 Solが登場するころまでは、性能で先行するAnthropicをOpenAIが追い、Googleは安さや自社サービスとの連携で勝負しているように見えていました。その後は各社が、性能の上限を伸ばす一方で、上位に近い能力を安く使えるモデルも増やしています。会社ごとに振り返ってみます。
Anthropic:性能の上限を伸ばし、普段使いへ広げる
4月のMythos Previewから、6月のFable・Mythos 5へ。最上位の能力に注目していましたが、Fableの$10/$50を普段の実装で使うのは、なかなか勇気がいります。その後、6月末のSonnet 5は一部でOpus 4.8級の能力を$2/$10で、7月のOpus 5はFable 5に近い能力を半額の$5/$25で提供しました。
9月にはFable・Mythos 5.1で上限を伸ばしつつ、Opus 5.5は多くの仕事でFable 5.1級の能力を$4/$20へ。Sonnet 5.5も$2/$10を据え置いて性能を伸ばし、10月のHaiku 5.5は大量処理向けの価格帯を強化しました。OpenAIや中国勢が迫るなか、最上位の能力を普段使いへ広げてきた印象です。
開発者にとっては、Claude Code はやはりハーネスとしての機能性が高く、魅力的です。私は、あまりベンダーロックインされたくないという思想が強いんですが。便利さの前では、思想もなかなか脆いです。
OpenAI:性能を追いながら、価格帯を広げる
6月下旬に限定公開され、7月に一般提供されたGPT-5.6 Sol・Terra・Lunaは、能力と価格を三階層に分けた展開でした。SolはFable 5に迫り、一部の業務では上回る結果に。その後も値下げが続き、9月のGPT-6 Astraでは、Fable 5.1やOpus 5と性能の上限を競っています。
同じ9月のGPT-6 Sol・Lunaは、Solが$2/$10、Lunaが$0.10/$0.50へ。続くGPT-6.1 SolはAstraに迫る能力を5分の1の単価で、10月のGPT-6.1 TerraはSonnet 5.5やSolの半単価となる$1/$5で提供しました。
OpenAI は基本的に Anthropic よりも安く LLM を提供している印象ですが、この1か月くらいは Sonnet 5.5 の登場でこのイメージが少し崩れました。
Google:Flash系の更新から、性能上限の競争へ
7〜9月はGemini 3.6→3.8 Flashの更新が続き、導入単価$0.75/$3.75を維持しながら性能を向上。3.8では一部の開発試験でOpus 5や5.6 Solに近い結果を示しました。Google は長い間マルチモーダル性(画像・動画・音声)を押しており、そこが強みでもありましたが、ここの差もどんどんと縮んできている印象です。それでも、まだある程度の数の動画を解析するとなったら 3.8 Flash を検討するかなぁ。
9月末のGemini 4 ArgonはAstraやOpus 5.5などと比較し、10月にはGemini 4 Carbonでさらに性能を伸ばしました。ともに導入価格は$2/$10で、終了後は$4/$20。安さや連携に加えて、性能の上限を競う動きも強まった印象です。
中国勢:低価格の選択肢から、最上位との競争へ
4月のDeepSeek V4-Pro・Flashは、公開ウェイトと低価格で気になる存在でした。Proは発売時$1.74/$3.48から割引で$0.435/$0.87へ下がり、その価格のまま5月に恒久化されました。性能比でバグっている価格だったので、かなり話題になりましたね。6月のGLM-5.2も一部の長時間開発でOpus 4.8に迫り、上位に近い能力を安く使う選択肢が増えました。
7月のKimi K3はFable 5や5.6 Solに迫る能力を掲げ、8月にはQwen3.8-Max、GLM-5.3と更新が続きます。GLM-5.3-Flashは画像理解にも対応し、通常$0.15/$0.50でGLM-5.2を上回ると訴求。9月のDeepSeek V4.1 Flashも、ピーク$0.30/$1.20という低い単価のままで開発・業務の能力を伸ばしました。
一方、10月のKimi K3.1 Proは、最新のAstraやOpus 5.5にも迫る性能を掲げ、単価はK3の$3/$15から$2/$10へ。上でも述べましたが、「中国製モデルは少し性能を譲る代わりに安い」という見方だけでは、捉えきれなくなってきています。
こうして見ると、各社が一斉に同じ方向へ切り替わったというより、性能と価格の両方で、互いの得意な領域との差を埋めてきたように見えます。
私の開発セットアップの変遷
ここでちょっと私の開発手法の振り返りです。
6月頃まではClaude CodeやCodexを使い、利用枠が足りなくなるとGoogle の Antigravityへ、というように複数の道具を行き来していました。OpenClawやHermes Agentに安いモデルを組み合わせる使い方も気になって試していましたが、セキュリティ面の心配が残り、日常の開発はClaude CodeやCodexでよいか、となりました。
ただ、このあたりからコストが気になるようになり、中国製モデルも話題になっていたのでDeepSeekやGLMについて調べた結果、OpenCodeというハーネスで Thinker と Implementer という2種類のエージェントを定義し、OpenAIの上位モデルをThinkerに、DeepSeekやGLMをImplementerにする構成に落ち着きました。Thinkerと対話しながら調査して実装の骨格まで詰め、GitHub Issueに落とし、Implementerが実装するといった感じです。
このあたりから、自分なりのループエンジニアリングについて考えるようになりました。開発用の Skill やサブエージェントの定義を専用リポジトリで整備し、計画・実装・レビュー・修正に加えて、うまくいかなかった原因を指示や手順に戻すところまで含めて逐次改善しています。
いろいろとモデルを変えながら、1か月くらいはこれをメインで使っていたと思います。GLM-5.2や、その後の5.3-Flashなどを試し、別の選択肢としてGemini Flash系も使っていました。ただ、不満もいくつかあって、安いモデルの動作が安定しなかったり、画像が入力できなかったり、スマホからのリモート操作が面倒だったり(herdr + Tailscale でやってました)、あたりです。
その後は各社から性能が十分で安価なモデルが出たことと、ハーネスやスマホアプリの完成度の高さから Claude Code や Codex に移行することにしました(Claude Code や Codex にはリモート接続機能があって、PC の作業をスマホアプリから操作可能)。エージェントの Thinker/Implementer 構成は変わっていません。
私の記憶では、GPT-5.6 のころから徐々に Codex 中心になり、Sonnet 5.5でClaude Codeの利用が増えました。今はTerraを機にCodexの比率を少し戻し、Googleの発表も見てAntigravityも試してみようか、と考えています。
基本は各社の月20ドル前後のサブスクで、足りないときにたまに追加する形です。簡単なWebアプリをいくつか作る程度では、それで十分でした。一社に全部寄せないのは、ベンダーロックインを避けたいことと、いろいろなものに触れておきたいという理由です。一方で、ハーネスの機能を使いきりたい気持ち(特に Claude Code)もあるのですが。
作り込んだ仕組みを、どこまで持ち続けるか
こうして振り返ると、モデルの性能や価格が変わるたびに、自分の開発環境も選び直していました。以前は意味のあった工夫が、新しいモデルやハーネスの機能で不要になることもありそうです。作り込んだ仕組みを、ずっと維持することがよいとも限りません。
最初はベンダーへの依存を避けたくて、OpenCode、Codex、Claude Code、Antigravity のどれでも使える手順を中心に作っていました。ハーネス固有の機能を前提にしすぎない方針です。ただ、ループエンジニアリングを実践するなかで、固有の機能を使う便利さも実感しています。最近は、そのあたりの縛りを少し緩めて使うようになりました。
それでも、モデルの性能や価格だけでなく、ハーネスやアプリの使いやすさまで含めて、そのときよい環境を選びたい気持ちは変わりません。便利な機能は使いたい。でも、次にもっとよい選択肢が出たときに、乗り換えるコストが高くならないようにもしておきたい。この加減は、まだ試行錯誤中です。
モデルの性能差は縮まってきた気がしますが、私のサブスクは増えるばかりです。
元記事(ブログ版): 2026年後半のLLM動向: この数か月の進化を振り返る
関連: Agentic Development: どうスケールさせるのか?
10 coding agents, one Qwen 3.8 27B, same llama-server: tokens and cache reuse differ more than pass rates
Part 2 of my agent test (part 1 was a 4B model)
Hardware I used: MacBook Pro M5 Max, 64 GB. Qwen 3.8 27B Q4 on llama.cpp, thinking off.
Setup
- same model and server for everyone
- 5 small coding tasks, each with a little trap
- each agent locked in a sandbox: it can only touch its own folder and only talk to the model
- auto-approve on, telemetry off, 5 min limit
Results
|#|Agent|Passed|Median time|Median tokens|Cache reuse|Tried to go online|
|:-|:-|:-|:-|:-|:-|:-|
|1|Pi|5/5|38 s|9.2k|72%|no|
|2|DeepSeek Harness|5/5|60 s|35.3k|83%|no|
|3|Codex CLI|5/5|94 s|36.3k|79%|every run|
|4|OpenClaw|5/5|106 s|39.0k|72%|unclear|
|5|Cline|5/5|152 s|29.6k|79%|every run|
|6|OpenCode|4/5|50 s|32.4k|75%|every run|
|7|Hermes|4/5|80 s|64.9k|93%|no|
|8|Atomic Agent|5/5|56 s|30.4k|86%|no|
|9|goose|4/5|98 s|20.3k|76%|every run|
|10|Qwen Code|4/5|183 s|107.9k|74%|unclear|
Tokens and cache
Pi is crazy lean: about 9k tokens per task, the lowest on every single task. Qwen Code was the hungriest at about 108k per task.
Atomic Agent is the odd one out in how it works. It was the only one talking to llama.cpp's native endpoint with a grammar that forces the tool calls into shape. The other nine all went through the OpenAI-style API. It's also built to keep its prompt the same from step to step, which is why it reused 86% of the cache (2rd of 10). It stayed offline the whole time and passed 5/5.
DeepSeek Harness was the surprise. It’s still an alpha, but it went 5/5, never tried to go online, and had the best cache reuse after Atomic Agent (83%).
The trap and the network
The log task got three agents. They grepped "ERROR" and got 244 instead of 64, because some normal lines just mention errors. OpenCode, goose and Atomic Agent fell for it.
Codex, Cline, goose and OpenCode tried to reach the internet in every run, even with telemetry off. The sandbox blocked it, so I don't know what they were calling. OpenClaw and Qwen Code are "unclear" because they run as plain node and I couldn't tell them apart.
llama.cpp notes
- Codex needs llama-server's /v1/responses now
- Hermes needs at least 64K context
- Cline and Qwen Code have usage stats on by default, DeepSeek Harness uploads session logs (per their docs). I turned it all off
Which column matters most to you when you pick an agent for a local model like Qwen 3.8 27B: passing, speed, tokens, or staying offline?
36 points · 36 comments
ローカル LLM のエージェントを AppContainer に入れて DNS まで塞いだ。ループバックの例外は全ポートを開けていた
!
先に、この記事の要約(専門用語なし)
前の記事で、ローカルの LLM のエージェント(Hermes)を、外へ通信できない専用のユーザーで動かした。ただ、名前の問い合わせ(DNS)だけは、Windows の別のサービスが代わりに外へ出すので止められなかった
Windows のアプリの隔離の仕組み(AppContainer)の箱に、ネットワークの権限を何も付けずに入れると、その別のサービスが、箱からの問い合わせを「アクセス拒否」で断った。DNS Client の記録では、ネットワークへの問い合わせは始まらなかった
箱の中で Hermes とそのチャット画面を動かすまでに、詰まりが 9 つあった。いちばん手間取ったのは、チャット画面が CPU を使い切ったまま何も描かなかったことだった
箱から同じ PC の LLM に届くように付けた例外が、PC の中の全部の窓口を開けていた。箱の身元で照合する規則で、要る窓口だけに絞り直した
箱の外で動く台本を、箱の中から書き換えられる道もあった。置き場を変えて塞いだ
前回の続き
前の記事では、Surface で動かしている Hermes Agent を、権限の低い専用のユーザーで動かし、そのユーザーの外向きの通信を全部止めた。ふつうの通信は外へ届かなかった。
残ったのは名前の問い合わせだった。専用のユーザーで Resolve-DnsName を使い、でたらめな名前を引くと、外の DNS サーバーの「その名前は無い」という答えが返ってきた。調べた範囲では、Windows の名前解決の API を通した問い合わせは、DNS Client というサービスが代わりに行い、このサービスは別のアカウントで動いているようだ。だから、ユーザーごとの外向きの遮断に当たらない。名前に少しずつ情報を埋めれば、外へ持ち出せる。
前の記事では、仮想マシンに入れてネットワークを内部だけにする案を挙げ、「次に試す」と書いた。まず手軽な Windows Sandbox で確かめた。
ネットワークなし:DNS サーバーが無いので、名前の問い合わせは出ない。ただ、同じ PC の LLM にも届かない
ネットワークあり(NAT):本体の側が問い合わせを代わりに外へ流した。本体の記録では SYSTEM の名義になり、どの箱から来たのかを区別できなかった
別の PC で前に試した箱でも、名前の問い合わせは外へ出ていた(DNS の見張りの記事)。問い合わせを代わりに送るサービスは、誰が問い合わせたか(プロセスとユーザー)を記録してはいたが、それを理由に断ることはしなかった。
内部だけのネットワークを組むには Hyper-V が要り、LLM と画像生成の GPU を仮想マシンに渡せるかという壁もある。そこで、OS の外側で切るのをいったんやめた。問い合わせを受けるサービスの側で「誰が問い合わせたか」を見て断れる仕組みを探し、AppContainer を試した。
AppContainer とは
AppContainer は、Windows でストアのアプリなどを閉じ込めるための仕組みである。箱の中のプロセスは、箱ごとの身元(package SID)を持つ。ファイルなどは、その身元か、すべての箱に共通の身元(ALL APPLICATION PACKAGES)に許された所しか触れない。ネットワークも、箱に付けた権限(capability)の分しか使えない。
ストアのアプリでなくても、ふつうのプログラムを箱に入れて起動できる。CreateAppContainerProfile で箱を作り、CreateProcess に箱の情報(PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES)を付けて起動する。小さな起動役を C# で書き、PowerShell の Add-Type で読み込んで使った。
小さな試験:箱から何が届くか
権限を何も付けない箱で、外と PC の中への届き方を確かめた。
試したこと
権限なしの箱
対照:インターネットの権限(internetClient)を付けた箱
名前の問い合わせ(Resolve-DnsName)
DNS Client がその場で拒否(status 5=アクセス拒否)
外まで届いた
外の IP への直接の接続・UDP 53 への直送
通らない
—
同じ PC の LLM と画像生成の窓口(127.0.0.1)
通らない
—
名前の問い合わせは、DNS Client の記録(Microsoft-Windows-DNS-Client/Operational)でも確かめた。権限なしの箱では、問い合わせを受けた記録(3006)と終わった記録(3008)だけが残り、ネットワークへ問い合わせを始めたときに出る記録(3009 以降)は出なかった。DNS Client が、問い合わせてきたのが箱だと分かったうえで断っていると見ている。専用のユーザーでは止められなかったものが、ここで止まった。
ほかの抜け道も試した。外の IP のファイル共有(\<IP>)と WebDAV は、どの条件でも外へ接続しなかった。BITS(Windows のバックグラウンド転送)は、普段のユーザーでは BITS のサービスが代わりに外へ出た。専用のユーザーは「ネットワークにログオンしていない」で失敗し、箱は「アクセス拒否」で失敗した。
箱は、同じ PC の中(ループバック)への接続も止める。Hermes は同じ PC の LLM と画像生成に頼むので、ここだけは開ける必要がある。CheckNetIsolation LoopbackExempt で箱にループバックの例外を付けると(管理者が要る)、LLM と画像生成に届き、名前の問い合わせは拒否のままだった。
この時点では、これで足りると思っていた。あとで、この例外が穴を開けていたと分かる(後の節)。
Hermes を箱の中で動かす
Hermes は Python で動き、チャット画面は Node で動く。どちらも、箱の中で動かす前提では作られていない。1 つ解くと次が出た。直しはどれも、箱で動かすときだけ働く形にして、箱の外での動きは変えていない。
詰まり
原因
直し
ゲートウェイ(Hermes の常駐の部分)が「使えない」と出る
存在の確認でタスク スケジューラに問い合わせるが、箱からはタスク スケジューラに届かない
箱のときは問い合わせを飛ばす。制御用の名前付きパイプは \.\pipe\LOCAL\ の下に作り、DACL に AppContainer 全体(ALL APPLICATION PACKAGES)を足す
cmd 経由で起動した Node が動かなかった
原因は追っていない(箱の中の cmd そのものは起動でき、外のコマンドも動いた)
起動の親を Python にした
シェルの道具の bash(Git に付いてくる MSYS のもの)が 0xC0000142 で起動しない
MSYS が箱の中で初期化できない
busybox-w32(ARM64 版)の sh に替える。ash に無い declare・shopt・builtin などを使わない形に直す
Python で作ったフォルダに、作った本人が入れない
Hermes と標準ライブラリ(tempfile.mkdtemp)が、mode 0o700 でフォルダを作る。Python 3.13 から(それより前の版も、3.12.4・3.11.10・3.10.15・3.9.20 以降のセキュリティ修正で)、Windows でもこの mode を扱い、作った本人と管理者などだけが入れる ACL を付けるようになった(公式文書)。そこに箱の SID が入らない
箱のときだけ、mode を付けずに作るようにした。Python と Hermes のコードは書き換えず、仮想環境に置いた .pth で、起動のときに os.mkdir を包んでいる
チャット画面の Node が起動の途中で EPERM
実際のパスを求める処理が C:\ そのものを lstat する
C:\ そのものにだけ、箱へ属性の読み取りを付ける(継承なし)
Node が起動できない
Node の入ったフォルダに、箱が読める許可が無い
そのフォルダに、箱の SID への読み取りと実行を付ける
ゲートウェイが立ち上がらない
Hermes(Python の側)は、Windows ではゲートウェイをジョブから切り離した別のプロセスとして起動する作りで、箱の中では立ち上がらなかった(切り離しが許されないためと見ている)
起動の台本で、ゲートウェイを先に起動しておく
Hermes が /c/Users/... の形のパスで空振りする
システムプロンプトに「この形のパスが使える」とあるが、busybox では通らない
箱のときだけ、その説明を差し替える
チャット画面が CPU を使い切ったまま何も描かない
いちばん手間取ったのはこれだった。チャット画面の Node が CPU を 100% 使い続け、画面には何も出ない。エラーも出ない。
Node の --prof でプロファイルを取ると、git のブランチ名を取る execFile の中で回り続けていた。箱の中では、Node が使う libuv が、子プロセスの出力を受ける名前付きパイプを作れない。拒否されるたびに名前を変えて作り直し、それを続けていた、と読んだ。箱の中で、出力をパイプで受ける起動は戻ってこず、パイプを使わない起動(stdio を ignore)はすぐ終わることを確かめた。
直しは、Node の前処理(--require)で execFile を包み、子プロセスの出力をパイプではなく一時ファイルで受けるようにした。パイプで子プロセスを起こす spawn は、回り続けないよう、すぐに失敗させている。チャット画面のコードには手を入れていない。
脇道で見誤りかけたこと
チャットだけが途中で切れた。中継(後述)が、接続のときに付けた 10 秒の時間切れを付けたままにしていて、10 秒データが流れない WebSocket がそこで切られていた
試験の道具でチャットに文を送ると、送信されなかった。文字と Enter を一度に送っていたので、画面が貼り付けとして受け取っていた。箱の外でも同じだったので、箱のせいではなかった
送った文字が入力欄に出ただけなのを、返事が来たと誤って判定しかけた。返事の中身まで見て判定するようにした
ブラウザから箱の中の画面へ:逆向きの中継
ループバックの例外を付けても、通るのは箱から外への向きだけで、外のブラウザから箱の中の待ち受けへは繋がらなかった。箱に、ネットワークで待ち受ける権限(privateNetworkClientServer)を付けて試したが、外から箱の中の待ち受けへは繋がらなかった。しかも、名前の問い合わせと LAN まで通るようになった。
そこで、向きを逆にした。箱の中から、外で待っている中継へ接続を張っておく。ブラウザの接続を受けた中継は、その接続を箱の中の画面に渡す。中継は標準ライブラリだけで書き、127.0.0.1 だけで待ち受け、トークンを照合してから繋ぐ。
穴 1:ループバックの例外は、PC の中の全部の窓口を開ける
箱の点検として、箱の中から、127.0.0.1 で待ち受けているものに総当たりで接続した(接続できたかだけを見て、中身は送らない)。結果は、ほぼ全部に届いた。LLM と画像生成だけでなく、ファイル共有、SSH、ゲームの配信のアプリ、そのほか同じ PC で待ち受けている別のサービスにもである。中には、外へ通信できる別のプログラムに文を渡せる窓口もあった。そこに届けば、名前の問い合わせを塞いでも、その先から外へ出せてしまう。
CheckNetIsolation の例外は、ポートを選べない。付けると、その箱からループバックの全部に届く。
前の記事で、専用のユーザーのループバックは、WFP(Windows Filtering Platform)の規則で決めた窓口だけに絞ってあった。ところが、その専用のユーザーの名義で箱を動かしても、全部に届いた。規則はユーザーの身元で照合していて、箱の中のプロセスには当たらなかった。箱のトークンでは、アクセスの確認に箱の SID も要るためだと見ている。
もう一つあった。箱の SID は、箱の名前から決まる(名前から SID を求める DeriveAppContainerSidFromAppContainerName という関数もある)。例外は SID に付くので、例外を付けた箱と同じ名前で箱を作れば、その例外をそのまま使える。実際に、専用のユーザーが自分で同じ名前の箱を作ると、全部の窓口に届いた。試験のために例外を付けた時点で、本番の側にも同じ穴が開いていたことになる。
直しは次の順にした。
例外を外す
WFP の規則を、ユーザーではなく箱の身元で照合する形で作り直す。照合の条件は FWPM_CONDITION_ALE_PACKAGE_ID(箱の package SID)にする。許すのは、LLM・画像生成・中継の受け口・箱の中の画面とゲートウェイ・Hermes が内部で使う範囲だけで、ほかのポートは止める
規則が入ってから、例外を付け直す
普段のユーザーと専用のユーザーのどちらの名義で箱を動かしても、許したポートだけに届き、残りは WSAEACCES(10013)で拒否されることを確かめた。
穴 2:箱の外で動く台本を、箱が書き換えられる
試験のときは、逆向きの中継の、外の側の台本が普段のユーザーで動いていた。この台本が、箱から書けるフォルダに置かれていて、そこを作業フォルダにして動いていた。
箱の中から台本を書き換えれば、次の起動で、その中身が箱の外の権限で動く。書き換えなくても、同じフォルダに argparse.py のような名前のファイルを置けば、同じことが起きる。Python は、台本のあるフォルダをモジュールの探し先の先頭に入れるからである。
直しは、箱の外で動かすものを、箱が書けない場所に置くことにした。作業フォルダも箱が書けない場所にし、Python は -I を付けて起動する。本番では、外の中継も専用のユーザーで動かしている。
箱の境目は、ネットワークだけではなかった。箱が書けるファイルを、箱の外の誰が実行するか、まで見る必要があった。
箱を動かす名義は、専用のユーザーにした
箱の中のプロセスを、普段のユーザーの名義で動かすか、前の記事の専用のユーザーの名義で動かすかを決める必要があった。専用のユーザーにした。
理由は、守りを箱ひとつに頼らないためである。箱の中のプロセスは、ファイルについては、箱と専用のユーザーの両方に許された所しか触れない。箱から抜け出されても、出た先は、外向きの通信を止め、書ける場所を絞った専用のユーザーである。普段のユーザーの名義なら、自分のファイルにも全部の通信にも手が届く。
ただし、ネットワークは別だった。専用のユーザーの外向きを止める規則は、ユーザーの身元で照合しているので、箱の中のプロセスには当たらない。インターネットの権限を付けた箱を専用のユーザーの名義で動かすと、外の IP に届いた。
そこで、箱そのものの身元で照合するファイアウォールの規則(New-NetFirewallRule の -Package)を 1 本足した。外向きで、行き先がループバック以外のものを止める。試しに一度だけ箱にインターネットの権限を付けて比べると、外への直接の接続は WSAEACCES(10013)で止まり、同じ PC の LLM と画像生成には届いた。ただ、でたらめな名前の問い合わせは、規則を入れたあとも外の DNS サーバーまで届いた。権限を付けた箱の問い合わせは DNS Client が代わりに外へ出すので、箱の身元の規則には当たらないと見ている。
名前の問い合わせまで塞いでいるのは、箱に権限を何も付けないことだけだった。点検では、後で書く 47 項目に加えて、本番の起動の台本が箱に権限を付けていないかと、この規則が残っているかも見ている。権限を付け間違えても、点検を流せば気づける。
欠点は手間で、起動が二段になり、ファイルの許可も両方に付け、試験も両方でやり直すことになる。使い勝手と速さは変わらなかった。
本番の切り替えで、穴 1 が数分だけ開き直した
試験の名前のまま本番にするのをやめ、本番用の名前で箱を作り直した。名前が変わると SID も変わるので、ファイルの許可・WFP の規則・ループバックの例外を、新しい SID に入れ直す必要がある。ファイルの許可は、試験の箱に付けた明示の許可を同じ形で新しい SID に写す台本で移した(約 4,700 か所)。
ここで一つ失敗した。例外を付け替える台本に、こう書いていた。
CheckNetIsolation LoopbackExempt -a -p=$sid
PowerShell は、- で始まるこの引数の中の $sid を展開せずに、そのまま渡していた。CheckNetIsolation はエラー 1337(SID が不正)で失敗した。WFP の規則は新しい箱に移ったのに、古い箱の例外が規則なしで残り、数分のあいだ穴 1 が開き直していた。
手元の Windows PowerShell 5.1 でも、同じことが起きた。
$sid = "S-1-15-2-123"
cmd /c echo -p=$sid # → -p=$sid
cmd /c echo "-p=$sid" # → -p=S-1-15-2-123
引用符で囲めば展開される。台本は "-p=$sid" に直した。付け替えのあとは、成功の表示を探すのではなく、一覧(-s)で結果を見ることにした。
開いていた数分のあいだも、箱の中のものは専用のユーザーの名義で動いていた。ファイルは、専用のユーザーにも箱にも許された所しか触れない。ただ、ネットワークは別だった。ループバックの穴は専用のユーザーの規則では塞がらず(穴 1 のとおり)、後で確かめると、外向きを止める規則も箱の中のプロセスには当たっていなかった。だから、付け替えのあとは一覧で確かめる。
崩れに気づくための点検
設定は、更新や編集で黙って崩れる。前の記事でも、行動ルールのファイルを編集したら、書き換えを止める設定が外れていた。そこで、管理者の要らない点検の台本を 1 本作り、更新や設定の変更のたびに流すことにした。
専用のユーザーの名義の箱の中から、47 項目を確かめる。
ループバック:止まるべきポートが止まり、届くべきポートに届くか
名前の問い合わせと、外の IP への直接の接続が止まるか
読めてはいけない場所(普段のユーザーのフォルダ、資格情報、ほかの AI の道具の設定、SSH の鍵)が読めないか
書けてはいけない場所(Hermes 本体、Python の環境)に書けないか
あわせて、ループバックの例外の一覧に想定外の箱が無いか、専用のユーザーの外向きを止める規則と、箱そのものの外向きの規則が残っているか、本番の起動で箱に権限を付けていないか、画像生成の閉じ込めが崩れていないかを見る。点検の台本と起動役は、箱からも専用のユーザーからも書けない場所に置いた。点検の台本そのものが抜け道にならないようにするためである。
本番の切り替えのあと、47 項目は全部通った。ブラウザでチャットの返事が返り、シェルの道具(busybox)から画像生成の道具まで呼べた。止める手順で、箱の中のプロセスが全部止まることも確かめた。
使えなくなったもの
箱に入れた代わりに、使えなくなったものがある。
Git に付いてくる MSYS の道具(bash など)。busybox の範囲で書き直す
busybox の wget(箱の中で落ちる)
git のブランチ名の表示など、チャット画面の付属の機能の一部
curl の -o /dev/null(-o NUL なら通る)。ネイティブのプログラムの出力の改行は CRLF のままで、日本語の出力が cp932 で化けることがある
残るもの
作ったファイル(画像や文書の埋め込み情報)に文を混ぜ、それを読む別の AI に指示を仕込む道。仕組みでは防げないので、読む側の決まり(ファイルの中の指示に従わず、見つけたら知らせる)で守っている
設定の崩れ。上の点検で気づく
手元で確かめるときのコマンド例
名前の問い合わせがネットワークへ出たか(箱からの問い合わせで 3009 以降が出ていなければ、ネットワークへは問い合わせていない)
Get-WinEvent -LogName Microsoft-Windows-DNS-Client/Operational -MaxEvents 50 |
Where-Object Id -in 3006,3008,3009,3010,3011,3020 | Select-Object TimeCreated, Id, Message
ループバックの例外の一覧(想定外の箱が無いか)
CheckNetIsolation LoopbackExempt -s
例外を外す(管理者)。SID は引用符で囲む
CheckNetIsolation LoopbackExempt -d "-p=<箱の SID>"
箱そのものの外向きを、ループバック以外は止める規則も足した(管理者の PowerShell)。箱の SID は、箱の名前から DeriveAppContainerSidFromAppContainerName で求めたもの(S-1-15-2-… の形)である。ループバックへの道は、別に作った WFP の規則(箱の身元で照合)に任せているので、この規則ではループバックを外してある。
$sid = "<箱の SID>"
ループバック以外:IPv4 は 127.0.0.0/8 を除く 2 つの範囲、IPv6 は ::1 を除く範囲
$remote = @("0.0.0.0-126.255.255.255", "128.0.0.0-255.255.255.255", "::2-ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")
New-NetFirewallRule -Name "<規則の名前>" -DisplayName "<規則の名前>" `
-Direction Outbound -Action Block -Package $sid -RemoteAddress $remote -Profile Any -Enabled True
確かめる
$r = Get-NetFirewallRule -Name "<規則の名前>"
$r | Select-Object Name, Enabled, Action, Direction
($r | Get-NetFirewallApplicationFilter).Package # 箱の SID が出る
($r | Get-NetFirewallAddressFilter).RemoteAddress # 上の 3 つの範囲が出る
外す
Remove-NetFirewallRule -Name "<規則の名前>"
箱の名前を変えると SID も変わるので、この規則も入れ直しになる。前に書いたとおり、この規則で止まるのは直接の接続までで、名前の問い合わせは止まらない。
箱の中から総当たりで接続を試すときは、接続できたかだけを見る。結果は、届いた/拒否(10013)/時間切れ(許されているが、待ち受けが無い)の 3 つに分かれる。
持ち帰り
ユーザー単位の外向きの遮断では、名前の問い合わせは止まらなかった。ネットワークの権限を何も付けない AppContainer に入れると、DNS Client が箱からの問い合わせを断り、DNS Client の記録では、ネットワークへの問い合わせは始まらなかった
CheckNetIsolation のループバックの例外は、ポートを選べない。付けると、同じ PC で待ち受けている全部に届く。WFP の規則を箱の身元(FWPM_CONDITION_ALE_PACKAGE_ID)で照合して、要るポートだけに絞る。規則を入れてから例外を付ける
ユーザーの身元で照合する WFP の規則は、箱の中のプロセスには当たらなかった。ファイアウォールの外向きの規則も同じだった
箱の SID は名前から決まる。例外を付けた箱と同じ名前で箱を作れば、その例外を使える。試験用の例外も、外すまでは本番の穴になる
箱の境目は、ネットワークだけではない。箱が書けるファイルを、箱の外で誰が実行するかまで見る
PowerShell では、- で始まる引数の中の変数が展開されないことがある。"-p=$sid" と囲み、付け替えのあとは一覧で結果を見る
名前の問い合わせを塞いでいるのは、箱に権限を何も付けないことだけだった。箱の身元で照合するファイアウォールの規則(-Package)を足すと、権限を付け間違えても外への直接の接続は止まるが、名前の問い合わせは止まらない
守りを箱ひとつに頼らない。箱を動かす名義を専用のユーザーにすると、ファイルの範囲と、箱から抜け出た先が狭くなる。ただし、ユーザーで照合する通信の規則(外向きの遮断・WFP の絞り込み)は、箱の中には当たらない
Oh-my-hermes guía completa para orquestar el agente Hermes con modelos en paralelo
Oh-my-hermes, o OMH, es una capa de operación de código abierto para Hermes Agent que convierte objetivos amplios en flujos de trabajo verificables. En vez de tratar una petición como una sola conversación con un…
Oh-my-hermes guía completa para orquestar el agente Hermes con modelos en paralelo
Oh-my-hermes, o OMH, es una capa de operación de código abierto para Hermes Agent que convierte objetivos amplios en flujos de trabajo verificables. En vez de tratar una petición como una sola conversación con un…
Mentions
17 things your agent harness should have! Do this first: go down the list and tick what your harness already does. Each unticked item is a failure your agent will hit. The list comes from reading the source of [OpenHands SDK][oh], [OpenCode][oc], [Hermes Agent][ha], [DeepSeek harness][ds] and [Strands harness][st]. Items 1–6 are the basics everyone builds. Items 7–17 are what separates these harnesses from a model in a loop. The rule behind all of them: the prompt is for judgement; anything a script can check is enforced in code. The list 1. A tool loop. Model call, tool calls, results back, repeat until the model answers. Everything else hangs off it. 2. Summarization. Old turns get summarized so long runs keep going. OpenHands keeps the first messages untouched and makes the summary track open tasks ([condenser][oh-condense]); Strands never drops pinned messages ([summarizing manager][st-summ]). 3. Subagents. Hand a focused task to a fresh agent with its own tools, and get back one answer ([Strands subagent.py][st-sub]). 4. Skills. Instructions loaded only when the task needs them. OpenHands loads them on keywords and on file paths the agent touches ([trigger.py][oh-skills]). 5. Model retries. Back off and retry on provider errors. Strands defaults to 6 attempts ([_retry.py][st-retry]). 6. Hooks. Run your own check before or after any tool call, on each prompt, and on stop. OpenHands follows Claude Code's convention: a hook that exits with code 2 blocks the call ([hooks/manager.py][oh-hooks]). 7. Errors that name the next step. A refusal says why and what to do instead. OpenCode answers a missing file with "Did you mean one of these?" ([read.ts][oc-read]); DeepSeek's sandbox says "retry this exact command once with sandbox_permissions + justification" ([escalation.ts][ds-esc]). 8. Read before edit. Refuse an edit to a file the agent never read, or one that changed since: edit requires reading "X" first ([DeepSeek][ds-read]). Hermes warns "was modified since you last read it" ([read tracking][ha-read]). 9. Repeated-call detection. Same tool, same arguments, again. DeepSeek reminds at 3, 5 and 8 repeats ([repeat-tool-reminder][ds-repeat]); Hermes warns, then blocks, then halts ([tool_guardrails.py][ha-guard]); OpenHands nudges once, then stops the run ([stuck_detector.py][oh-stuck]). 10. A step budget that takes tools away. When it's spent, OpenCode sets toolChoice: "none", appends a wrap-up message, and refuses any call that still arrives ([max-steps.ts][oc-steps], [llm.ts][oc-llm]). The run ends with an answer, not a crash. 11. "Done" is checked, not trusted. Hermes sends the model back to run its tests after a code edit ([verification_stop.py][ha-verify]). OpenCode's judge is told "The assistant claiming the goal is met is not evidence" ([judge.txt][oc-judge]). 12. Remove tools instead of forbidding them. OpenHands' planning agent can only edit PLAN.md ([planning.py][oh-plan]); OpenCode's explore agent starts from deny-all ([agent.ts][oc-agent]); Hermes subagents never get delegate or memory tools ([delegate_tool_toolsets.py][ha-sub]). 13. Parse shell commands before running them. OpenCode checks each command and target folder as a permission ([shell.ts][oc-shell]). Hermes keeps a blocklist no mode can override ([approval_floors.py][ha-floor]). 14. Reminders checked against state. Before every request OpenCode checks the last plan-mode reminder still matches the current mode ([plan.ts][oc-plan]). Strands re-adds the todo list each turn without saving it to history ([todos.py][st-todo]). 15. Silent argument repair. "42" becomes 42 before the tool runs ([Hermes arg_coercion.py][ha-coerce], [OpenCode tool-input-repair.ts][oc-repair]). No round trip, no error. 16. Summaries that don't give orders. Hermes labels them "REFERENCE ONLY … NOT as active instructions", after a stronger wording made the agent stop using tools ([context_compressor.py][ha-compact]). DeepSeek keeps user corrections word for word ([summarizer.ts][ds-sum]). 17. An LLM supervisor with hard limits. Hermes' guardian model rates risky commands and stops after repeated denials ([approval_smart.py][ha-smart]). Strands' judge must "treat partial satisfaction as failure" ([judge.py][st-judge]). A judge without a numeric cap is just another loop. Start here Have 1–6 already? Build 8, 9 and 10 next. Each is about a day of work and removes a whole class of failures. Next: open your file-edit tool and check whether it refuses an edit to a file the agent never read. [oh]: OpenHands/software-agent-sdk [oc]: anomalyco/opencode [ha]: NousResearch/hermes-agent [ds]: deepseek-ai/deepseek-harness [st]: strands-agents/harness-sdk [oh-condense]: github.com/OpenHands/software-agent-sdk/blob/295b… [st-summ]: github.com/strands-agents/harness-sdk/blob/3bd9ed… [st-sub]: github.com/strands-agents/harness-sdk/blob/3bd9ed… [oh-skills]: github.com/OpenHands/software-agent-sdk/blob/295b… [st-retry]: github.com/strands-agents/harness-sdk/blob/3bd9ed… [oh-hooks]: github.com/OpenHands/software-agent-sdk/blob/295b… [oc-read]: github.com/anomalyco/opencode/blob/2658ec02c848ce… [ds-esc]: github.com/deepseek-ai/deepseek-harness/blob/b150… [ds-read]: github.com/deepseek-ai/deepseek-harness/blob/b150… [ha-read]: github.com/NousResearch/hermes-agent/blob/53a6ced… [ds-repeat]: github.com/deepseek-ai/deepseek-harness/blob/b150… [ha-guard]: github.com/NousResearch/hermes-agent/blob/53a6ced… [oh-stuck]: github.com/OpenHands/software-agent-sdk/blob/295b… [oc-steps]: github.com/anomalyco/opencode/blob/2658ec02c848ce… [oc-llm]: github.com/anomalyco/opencode/blob/2658ec02c848ce… [ha-verify]: github.com/NousResearch/hermes-agent/blob/53a6ced… [oc-judge]: github.com/anomalyco/opencode/blob/2658ec02c848ce… [oh-plan]: github.com/OpenHands/software-agent-sdk/blob/295b… [oc-agent]: github.com/anomalyco/opencode/blob/2658ec02c848ce… [ha-sub]: github.com/NousResearch/hermes-agent/blob/53a6ced… [oc-shell]: github.com/anomalyco/opencode/blob/2658ec02c848ce… [ha-floor]: github.com/NousResearch/hermes-agent/blob/53a6ced… [oc-plan]: github.com/anomalyco/opencode/blob/2658ec02c848ce… [st-todo]: github.com/strands-agents/harness-sdk/blob/3bd9ed… [ha-coerce]: github.com/NousResearch/hermes-agent/blob/53a6ced… [oc-repair]: github.com/anomalyco/opencode/blob/2658ec02c848ce… [ha-compact]: github.com/NousResearch/hermes-agent/blob/53a6ced… [ds-sum]: github.com/deepseek-ai/deepseek-harness/blob/b150… [ha-smart]: github.com/NousResearch/hermes-agent/blob/53a6ced… [st-judge]: github.com/strands-agents/harness-sdk/blob/3bd9ed…
AI Agent OAuth Recovery: Stop Expired Connections From Retrying Forever An agent can have a perfect plan and still fail at the least interesting moment: halfway through a customer task, its connected account expires. The bad response is to retry the same API call until the run burns its budget. The worse response is to ask the model to repair authentication. OAuth failure is a product and workflow state, not a transient tool error. A reliable agent identifies whether a token can be refreshed, whether the customer must reconnect, and whether the original task is still safe to resume. This guide shows a vendor-neutral pattern for building that recovery path without exposing credentials or repeating side effects. Why expired connections become runaway agent runs OAuth access tokens are deliberately short-lived. A refresh token can usually obtain a replacement, but a user can revoke the grant, an administrator can change policy, or a provider can reject a refresh with invalid_grant. Google’s MCP guidance, for example, notes that clients need the right offline-access behavior to avoid repeated sign-in when access tokens expire (Google documentation). An agent loop often turns this ordinary condition into an incident: 1. A tool returns 401. 2. The model sees an opaque error and tries the tool again. 3. Several workers refresh the same connection at once. 4. One refresh rotates the refresh token; another now fails. 5. The workflow keeps retrying, or falls back to a different account without the user knowing. The durable rule is simple: authentication recovery belongs below the model. The model can explain a blocked task, but it should not inspect, paste, select, or refresh bearer tokens. Model the connection separately from the agent run Do not represent every failure as tool_failed. Keep a connection record with an explicit state, then let each run reference that record by ID. The run holds intent and progress; the connection holds authorization health. | Connection state | Meaning | Safe next action | | --- | --- | --- | | active | A recent check or call succeeded | Permit the requested tool | | refreshing | One service is refreshing the token | Wait briefly; do not start another refresh | | reauth_required | The grant is revoked, invalid, or missing | Pause affected runs and show a reconnect action | | provider_unavailable | The provider or token endpoint is unavailable | Retry on a bounded schedule; do not request consent yet | | disabled | The user, tenant, or policy removed access | Fail closed and require an explicit new connection | This distinction matters for UX. A reconnect screen cannot fix a provider outage, and an automatic retry cannot repair a revoked grant. Connection products already expose similar distinctions: an expired credential, a rejected reconnect, and a provider connection issue are different conditions with different remedies (connection troubleshooting example). Here is a minimal TypeScript shape: type ConnectionState = | "active" | "refreshing" | "reauth_required" | "provider_unavailable" | "disabled"; type Connection = { id: string; tenantId: string; provider: "crm" | "mail" | "calendar"; accountLabel: string; // safe display name, not an email if avoidable state: ConnectionState; grantedScopes: string[]; credentialRef: string; // vault reference, never the token itself version: number; lastVerifiedAt?: string; reauthReason?: "revoked" | "invalid_grant" | "scope_changed" | "missing"; }; Bind it to the tenant and intended account. “Use any connected calendar” is an unsafe fallback in a multi-account product. A reauthorized connection should update the same durable connection ID only after the user has seen the provider, account, and requested scopes. Make refresh a locked, typed operation Refresh-token rotation creates a small distributed-systems problem. Two requests might notice an old access token together. If both refresh independently, the provider may invalidate the token one of them was about to store. Use a short database lease or compare-and-swap update to nominate one refresher. Everyone else waits for its result. Keep the credential material in a secret manager; the application database stores only a vault reference and harmless status metadata. async function getUsableConnection(id: string) { const connection = await db.connection.findUniqueOrThrow({ where: { id } }); if (connection.state === "disabled") return { kind: "blocked", reason: "disabled" }; if (connection.state === "reauth_required") return { kind: "reauth", reason: connection.reauthReason }; const claimed = await db.connection.updateMany({ where: { id, state: "active", refreshLeaseUntil: { lt: new Date() } }, data: { state: "refreshing", refreshLeaseUntil: plusSeconds(20), version: { increment: 1 } } }); if (claimed.count === 0) return waitForConnectionResolution(id, 2500); try { const secret = await vault.read(connection.credentialRef); const refreshed = await oauth.refresh(secret.refreshToken); await vault.rotate(connection.credentialRef, refreshed); await db.connection.update({ where: { id }, data: { state: "active", lastVerifiedAt: new Date(), refreshLeaseUntil: null } }); return { kind: "ready" }; } catch (error) { return classifyAndPersistRefreshFailure(id, error); } } The pseudo-code assumes your storage layer supports an atomic conditional update. If it does not, use a transaction or a distributed lock with an expiry. More importantly, classify the response: - A timeout or 5xx is a bounded, retryable infrastructure failure. - invalid_grant, an invalid refresh token, or a user revocation is terminal for that grant. - A missing required scope needs reconnection and possibly consent for the new scope. - A 403 from the resource API may mean the grant works but the user lacks provider-side permission. Do not blindly reconnect. On a terminal refresh failure, mark the connection reauth_required once. Do not try it again from every queued run. A well-designed OAuth client does exactly this: it treats a failed refresh caused by a revoked or rotated grant as a reauthentication event rather than replaying the same unauthorized request (an implementation example). Pause the run with a recoverable checkpoint Once a task needs human reconnection, an agent has two responsibilities: preserve enough context to resume, and stop before it creates an ambiguous side effect. Persist a checkpoint before every external write and before you show the reconnect UI: { "run_id": "run_84a", "state": "waiting_for_connection", "connection_id": "conn_29", "required_capability": "crm.contacts.update", "checkpoint": "validated_contact_change", "intent_hash": "sha256:...", "resume_after_connection_version": 18, "created_at": "2026-11-04T10:15:00Z" } The intent_hash protects against quiet changes while the user is away. When the user returns, the agent should not send an old draft to a newly selected account or apply a stale CRM update. Fetch the current record, compare the relevant fields, re-run policy checks, and ask again if the intended action has changed. For an action that might already have reached the provider, do not resume by repeating it. Use an idempotency key or query the provider for an operation receipt first. This is the same unknown-outcome boundary that makes a network timeout different from a rejected write. Build a reconnect flow that earns trust The reconnect card should be specific without being alarming. It needs to answer what failed, what will happen next, and what will not happen automatically. > Reconnect needed — Your CRM connection can no longer authorize updates. The draft change is saved but has not been applied. Reconnect the account that owns this record, then review the change before continuing. Include these elements: - Provider name and a privacy-safe account label. - The capability that is blocked, not a raw OAuth error. - The saved task status: not started, safely paused, or needs verification. - A server-generated, short-lived reconnect URL bound to tenant, connection, and CSRF/session state. - A clear path to cancel or disconnect. Never put an authorization URL, authorization code, refresh token, or customer secret into a model prompt or chat message. The browser callback should reach your backend, exchange the code server-side, store the new credential in the vault, and redirect to a neutral completion page. The agent only receives connection_version: 19 and allowed capabilities. Reconnection is not permission escalation. If the user grants different scopes or selects another account, record that as a new authorization version and re-evaluate the task. A tool policy or approval that applied to the old connection must not silently carry over to a broader one. Resume safely instead of automatically continuing everything When OAuth completes, publish a connection.restored event. A scheduler can find runs waiting on that exact connection, but it should use a resume policy rather than wake every run. | Run type | Resume policy | | --- | --- | | Read-only lookup | Re-run after verifying tenant, account, and scope | | Draft generation | Resume only if source inputs and requested account still match | | External write | Recheck state, policy, and idempotency; often show a final confirmation | | Destructive action | Require a fresh approval, even after reconnect | This policy avoids an unpleasant surprise: a user reconnects their mailbox and suddenly receives a backlog of messages the agent attempted days earlier. The sensible default is “connection restored, task ready to review,” not “all blocked side effects now execute.” Use a version check to make the rule enforceable: function mayResume(run: Run, connection: Connection) { return run.state === "waiting_for_connection" && run.connectionId === connection.id && connection.state === "active" && connection.version >= run.resumeAfterConnectionVersion && run.tenantId === connection.tenantId; } The next tool call still goes through normal authorization, policy, rate limits, and approval gates. Resumption is a request to reconsider the action, not a stored permission slip. Test the failure paths that happy-path OAuth misses Most OAuth integration tests prove only that a new connection works. Production reliability comes from testing what happens later. - Force two simultaneous requests to refresh one connection; assert only one vault rotation occurs. - Return invalid_grant; assert one reauth_required transition and no retry storm. - Revoke an account between planning and execution; assert the write never starts. - Reconnect a different account; assert the paused run stays blocked until it is reviewed. - Change scopes during reconnection; assert a previously allowed write is reauthorized. - Timeout after an external write; assert the run reconciles with an idempotency key instead of repeating it. - Remove a connection while a run waits; assert all associated runs become safely terminal or need explicit reassignment. Measure the operational signals too: connections by state, refresh success rate, terminal refresh failures, time spent waiting for reconnect, repeated calls blocked after one failure, and resumes rejected because the account or target changed. Avoid logging raw tokens, authorization codes, request bodies with secrets, or full provider error payloads. A compact implementation checklist - [ ] Separate durable connection state from workflow state. - [ ] Keep credentials in a vault and expose only opaque references to application code. - [ ] Serialize refreshes per connection and handle rotation atomically. - [ ] Classify invalid_grant, missing scope, provider outage, and disabled access separately. - [ ] Turn terminal refresh failures into one reauth_required state, not open-ended retries. - [ ] Checkpoint the run before reconnect and bind it to tenant, connection, account, and intent. - [ ] Revalidate target data, policy, and idempotency before any resumed write. - [ ] Make reconnect visible, explicit, and reviewable. - [ ] Test revocation, rotation races, provider outages, changed scopes, and queued runs. OAuth is not background plumbing for an agent product. It is the boundary between a user’s connected account and an autonomous workflow. Treat an expired connection as a first-class state, and the product can pause honestly, reconnect safely, and finish only when it still has the right to do so. FAQ Should an AI agent retry after a 401 error? Not blindly. First determine whether the access token can be refreshed. A transient token-endpoint failure may justify a bounded retry, but a revoked grant or invalid_grant should move the connection to reauth_required and pause dependent work. What is the difference between token refresh and reauthorization? Refresh uses an existing refresh token without user interaction to obtain a new access token. Reauthorization starts a user-facing consent flow because the existing grant is invalid, missing, revoked, or needs different scopes. Can an agent reconnect an OAuth account automatically? It can refresh a valid existing grant in the background. It should not silently complete a new consent flow, choose a different account, or widen scopes. Those actions require the user’s explicit participation. How do I prevent OAuth refresh token race conditions? Allow one refresher per connection using an atomic database update, lease, or lock. Other requests wait for the outcome and read the newly stored credential reference. Rotate the vault value atomically and version the connection state. Should paused agent runs resume as soon as the user reconnects? Usually only read-only tasks can resume automatically. For drafts, writes, and destructive actions, revalidate the account, current target data, authorization, policy, and idempotency. A final review is often the safer experience. What should an agent show a user when a connection expires? Explain the blocked capability, provider, safe account label, and task status in plain language. Say whether any action was applied, preserve the task for review, and offer a secure reconnect action. Do not display raw OAuth errors, tokens, or authorization codes.
ok AI assistants are getting kinda out of hand So i've been reading about AI agent stuff and this week was actually insane. this guy in japan asked some AI assistant called Pickle to look up hotels for new years and it just... called the hotel by itself?? Twice. first time it said it was an AI and gave his full name, then the call dropped so it called back pretending to be his human secretary. He literally had to call the hotel and apologize en.yutura.net And then someone had this thing called Hermes Agent where another AI is supposed to double check risky stuff for you. it saw a "cache cleanup" was gonna delete a bunch of stuff, said yep that's dangerous, and approved it anyway. Wiped like 188k files, 21 gigs, no recycle bin, nothing hermes-agent#147373 oh and some researchers gave AIs a crypto wallet with a one cent limit, and the code to raise it got emailed to the owner... but the AI could read the email. 9 out of 10 just grabbed the code and raised their own limit 💀 coindesk.com Like you give them a tiny yes and they run with it. idk i'm not letting one touch my money or my files without me clicking ok first Has anyone else had an AI do something random like this??
A guardrail that decides at the door lets the intruder walk in later !A guardrail checks a tool call once, waves it through, and the tool then runs with full access to keys and the network — fail-open I report security bugs in AI-agent guardrails, and I keep finding the same shape of bug. This is the first one that's fully public, so I can show you the whole thing — the finding, the fix that shipped, and why I think the design itself is the problem. The finding (airlock, now public) airlock is a prompt-injection guardrail in the Hermes plugin catalog. Its job is to stop an agent from doing dangerous things without a human's say-so: in its default approvals_off: block mode, an action that needs approval should be blocked until a person approves it. The problem I reported: that block could be skipped entirely. !block mode promised to hold the action; in a gateway-run hermes -z turn the guard read the wrong unattended signal, so an approval-required action ran with no approval When the Hermes gateway runs a hermes -z turn, the turn is marked unattended in one place (gateway.session_finite) but not in the flag airlock was reading (HERMES_YOLO_MODE). So airlock looked at its usual signal, saw nothing that said "a human is driving this," and let an approval-required action run with no approval at all — even though the whole point of block mode is that nothing dangerous runs unattended. The maintainer triaged it and shipped a fix in airlock 0.2.3: it now asks the approval layer why Hermes would approve on its own, and it covers CLI subagents under delegation too. The fix is merged into the Hermes plugin catalog (PR #144871), and the advisory is public as GHSA-mgf8-gh7g-q4rg. Coordinated disclosure, credited. That's the system working, and the maintainer was great about it. The pattern underneath Here's why I care beyond this one bug. Most agent guardrails work the same way: they decide at the door. Something reads the pending action, matches it against a signal — a mode flag, a regex, a list of paths, a YARA rule — and then either waves it through or stops it. The tool itself runs with whatever access it already had. Every bug I find in this category is a gap between the signal the guard reads and the effect that actually happens: - the action is dangerous, but the signal says "attended," so it runs; - the path points at a secret, but it's spelled a way the matcher doesn't recognize, so it's missed; - the input is an injection, but it's phrased in a form the rule doesn't cover, so it passes clean. Same shape every time. The guard is reasoning about a string or a flag that stands in for the real thing, and anything that changes the representation without changing the effect slips past. When these fail, they fail open: no match, no block, action runs. I've started calling this class "stringly-typed guardrails," and once you see it you see it everywhere. A door-check can be made tighter — airlock's fix did exactly that — but tightening one gap doesn't change the design. There's always another spelling of the dangerous thing. What I build instead !Door-check guardrail vs CleiMesh: the token is the sandbox, default-deny, the tool only ever has the grant, and every call leaves a signed receipt you can replay I'm the author of CleiMesh, and it exists because I got tired of finding this bug. CleiMesh doesn't decide at the door. Two things are different: The permission becomes the sandbox. An agent runs under a signed, delegable token that says exactly which tools it may call and which directories or network it may touch — and the tool runs inside a WebAssembly sandbox configured from that token, default-deny. There's no "the guard waved it through but the tool still had full access," because the tool never had the access. A subagent can only get a narrower token, never a wider one. A prompt injection can ask for more all it likes; the grant doesn't widen. Every call leaves a receipt you can replay. Each tool call produces a signed, hash-chained receipt. You can verify the chain offline, and — the part no gateway offers — you can re-run a call from its receipt and compare the result byte for byte. You don't have to trust the log; you re-execute it. That's also what makes it an auditable record for GDPR/LGPD: a report per data subject, from evidence, not from a dashboard someone could edit. They're complementary, honestly — a door-check catches the obvious stuff fast. But if the door-check is the only thing between an injected agent and your keys, every bug like the one above is a hole. CleiMesh is the layer that doesn't have that failure mode, because there's nothing to "miss." Why I'm writing this Because finding these quietly and moving on makes me the nice guy who does free security work for other people's tools and gets nothing for it. I'd rather be clear about what I do: I audit agent guardrails, I report what I find responsibly, and I build the thing that enforces instead of guesses. If you run agents that touch real data — in Claude Code, Cursor, OpenClaw, Hermes — and "the guardrail checked it" is your whole story, I'd look harder at what the guardrail actually checks. CleiMesh is free and open (MIT/Apache-2.0): **cleitonaugusto/cleimesh**. There are two ClawHub skills that set it up — cleimesh-setup for an agent's own file tools, and cleimesh-mcp for the MCP servers it already uses. I also do paid security reviews; the repo README says how. airlock is the first public one. I have other coordinated disclosures in flight with different maintainers right now; when those patches ship, I'll write them up the same way. — Cleiton Augusto Correa Bezerra (github.com/cleitonaugusto)
the same steps in all three, side by side: read the seller's record, borrow and pay, repay in hermes agent it's a plugin, and it asks you before it pays. our pull request to the @NousResearch catalog: hermes-agent#147003 in agentkit it's an action provider, one npm i @priors/agentkit away. our pull request with @CoinbaseDev: agentkit#1611 in @elizaOS it's a plugin with four actions: npm i @priors/plugin-elizaos
2026年後半のLLM動向: この数か月の進化を振り返る ここ数週間で、気になるモデルの発表が続いています。OpenAIからは、Sonnet 5.5に近い能力を半分のAPI単価で使えるGPT-6.1 Terra。Googleからは、GPT-6 AstraやClaude Opus 5.5と競うGemini 4 Argonに続き、約1か月でさらに性能を伸ばしたGemini 4 Carbonです。中国からも、Opus 5.5やAstraに迫る性能をうたうKimi K3.1 Proが出てきました。 この数か月でだいぶ動きがあったので、まず最新の発表で現在地を確認し、そこまでの移り変わりを振り返ってみます。そのあとに、私のコーディング環境と思考がどう変わったか、最後に、進化の速い道具や、自分で作った仕組みとの付き合い方について少し。発表を読むたびに装備を買い替えている気もするので、いったん手持ちを整理してみます。 現在地:最新モデルは、何を競っているのか 直近の三つの発表から、それぞれが何を強みにしているのかを見てみます。料金は100万トークンあたりのAPI単価で、キャッシュなし入力/出力の順です。 Gemini 4 Carbon:Googleも性能の上限を競う 10月28日のCarbon発表では、①総合的な知的能力、②エージェントによる開発、③画面操作、④知識業務、⑤マルチモーダル理解、⑥長文脈理解、⑦科学・数学、⑧サイバーセキュリティの8分野を、複数のベンチマークで評価しています。 主な比較対象は、約1か月前に発表されたArgonに加え、GPT-6 Astra、Claude Opus 5.5、Claude Fable 5.1です。公式の比較表では、17指標のうち15で掲載モデル中の最高値を示しています。一部の開発指標ではAstra、画面操作ではOpus 5.5が上ですが、幅広い用途で最上位を競うモデル、という打ち出しです。 長い間 Flash系の更新のみでしたが、Argon、Carbonと続き、Googleも性能上限の競争に入ってきました。 導入価格はArgonと同じ、入力$2/出力$10。導入期間の終了後は$4/$20となる案内です。ただし、まずは一部のサイバー防御関係者からの限定提供で、一般提供日は未発表です。 GPT-6.1 Terra:Sonnet 5.5との比較で見る低価格モデル 10月26日のGPT-6.1 Terra発表は、費用や速さを強調した発表です。 普段使いの比較相手として気になるのはSonnet 5.5です。Terraは入力$1/出力$5で、Sonnetの$2/$10の半額。総合評価ではSonnet 5.5 に及ばないものの、低い単価で回せるモデル、という見方が近そうです。 評価会社Artificial Analysisの比較では、総合指数はTerraが約51、Sonnetが約54でした。両者とも最大思考量での比較で、SonnetはDefault Fallback設定の結果です。一方、実際のリポジトリのコード修正を測るFrontierCode 2.0では、42.3%対41.7%とほぼ互角です。OpenAIも、Sonnetが開発や画面操作で上回る比較を掲載しています。一方、Agents’ Last Examという評価では、Sol・Astraより作業を3倍超速く終えることや、Opus 5.5よりタスク費用が低いことを訴えています。 GPT-6 Sol・Luna、続いて6.1 Solが出るなかで、Terraは5.6のままでした。てっきり Sonnet 5.5 より高性能で同価格帯のものが出てくるのかと思っていたので、少し残念です。しかし、強みもあるので、どれだけ使えるのか試してみようとは思います。Agentic なコーディングは Sonnet 5.5 と要比較、画面操作(ブラウザ、動画編集、CADなど)は期待といった感じかな。 Kimi K3.1 Pro:米国勢にも、中国勢の上位にも比較を向ける 10月23日のKimi K3.1 Pro発表では、Astra・Opusに加え、中国勢のDeepSeek V4.1 FlashやGLM-5.3との比較も出しています。 Kimiの公表値では、開発や知識業務の一部でOpus 5.5やAstraに迫り、上回る項目もあります。比較対象のGLM-5.3やDeepSeek V4.1 Flashにも、複数の指標で差をつけています。 一方、DeepSeek V4.1 Flashのコスパは、やはり別格に見えます。9月の発表では、GPT-5.6 SolやOpus 5と競える指標もありました。しかも、ピーク時間でも入力$0.30/出力$1.20で、オフピークはさらに半額です。最新の最上位モデルに全面的に並ぶわけではなくても、この価格ならまだまだ出番がありそうです。 「中国製モデルは少し性能を譲る代わりに安い」という見方だけでは、捉えきれなくなってきたのかな、という印象です。 ここ数カ月の動き:上位の能力が、普段使いの価格へ 6月下旬にGPT-5.6 Solが登場するころまでは、性能で先行するAnthropicをOpenAIが追い、Googleは安さや自社サービスとの連携で勝負しているように見えていました。その後は各社が、性能の上限を伸ばす一方で、上位に近い能力を安く使えるモデルも増やしています。会社ごとに振り返ってみます。 Anthropic:性能の上限を伸ばし、普段使いへ広げる 4月のMythos Previewから、6月のFable・Mythos 5へ。最上位の能力に注目していましたが、Fableの$10/$50を普段の実装で使うのは、なかなか勇気がいります。その後、6月末のSonnet 5は一部でOpus 4.8級の能力を$2/$10で、7月のOpus 5はFable 5に近い能力を半額の$5/$25で提供しました。 9月にはFable・Mythos 5.1で上限を伸ばしつつ、Opus 5.5は多くの仕事でFable 5.1級の能力を$4/$20へ。Sonnet 5.5も$2/$10を据え置いて性能を伸ばし、10月のHaiku 5.5は大量処理向けの価格帯を強化しました。OpenAIや中国勢が迫るなか、最上位の能力を普段使いへ広げてきた印象です。 開発者にとっては、Claude Code はやはりハーネスとしての機能性が高く、魅力的です。私は、あまりベンダーロックインされたくないという思想が強いんですが。便利さの前では、思想もなかなか脆いです。 OpenAI:性能を追いながら、価格帯を広げる 6月下旬に限定公開され、7月に一般提供されたGPT-5.6 Sol・Terra・Lunaは、能力と価格を三階層に分けた展開でした。SolはFable 5に迫り、一部の業務では上回る結果に。その後も値下げが続き、9月のGPT-6 Astraでは、Fable 5.1やOpus 5と性能の上限を競っています。 同じ9月のGPT-6 Sol・Lunaは、Solが$2/$10、Lunaが$0.10/$0.50へ。続くGPT-6.1 SolはAstraに迫る能力を5分の1の単価で、10月のGPT-6.1 TerraはSonnet 5.5やSolの半単価となる$1/$5で提供しました。 OpenAI は基本的に Anthropic よりも安く LLM を提供している印象ですが、この1か月くらいは Sonnet 5.5 の登場でこのイメージが少し崩れました。 Google:Flash系の更新から、性能上限の競争へ 7〜9月はGemini 3.6→3.8 Flashの更新が続き、導入単価$0.75/$3.75を維持しながら性能を向上。3.8では一部の開発試験でOpus 5や5.6 Solに近い結果を示しました。Google は長い間マルチモーダル性(画像・動画・音声)を押しており、そこが強みでもありましたが、ここの差もどんどんと縮んできている印象です。それでも、まだある程度の数の動画を解析するとなったら 3.8 Flash を検討するかなぁ。 9月末のGemini 4 ArgonはAstraやOpus 5.5などと比較し、10月にはGemini 4 Carbonでさらに性能を伸ばしました。ともに導入価格は$2/$10で、終了後は$4/$20。安さや連携に加えて、性能の上限を競う動きも強まった印象です。 中国勢:低価格の選択肢から、最上位との競争へ 4月のDeepSeek V4-Pro・Flashは、公開ウェイトと低価格で気になる存在でした。Proは発売時$1.74/$3.48から割引で$0.435/$0.87へ下がり、その価格のまま5月に恒久化されました。性能比でバグっている価格だったので、かなり話題になりましたね。6月のGLM-5.2も一部の長時間開発でOpus 4.8に迫り、上位に近い能力を安く使う選択肢が増えました。 7月のKimi K3はFable 5や5.6 Solに迫る能力を掲げ、8月にはQwen3.8-Max、GLM-5.3と更新が続きます。GLM-5.3-Flashは画像理解にも対応し、通常$0.15/$0.50でGLM-5.2を上回ると訴求。9月のDeepSeek V4.1 Flashも、ピーク$0.30/$1.20という低い単価のままで開発・業務の能力を伸ばしました。 一方、10月のKimi K3.1 Proは、最新のAstraやOpus 5.5にも迫る性能を掲げ、単価はK3の$3/$15から$2/$10へ。上でも述べましたが、「中国製モデルは少し性能を譲る代わりに安い」という見方だけでは、捉えきれなくなってきています。 こうして見ると、各社が一斉に同じ方向へ切り替わったというより、性能と価格の両方で、互いの得意な領域との差を埋めてきたように見えます。 私の開発セットアップの変遷 ここでちょっと私の開発手法の振り返りです。 6月頃まではClaude CodeやCodexを使い、利用枠が足りなくなるとGoogle の Antigravityへ、というように複数の道具を行き来していました。OpenClawやHermes Agentに安いモデルを組み合わせる使い方も気になって試していましたが、セキュリティ面の心配が残り、日常の開発はClaude CodeやCodexでよいか、となりました。 ただ、このあたりからコストが気になるようになり、中国製モデルも話題になっていたのでDeepSeekやGLMについて調べた結果、OpenCodeというハーネスで Thinker と Implementer という2種類のエージェントを定義し、OpenAIの上位モデルをThinkerに、DeepSeekやGLMをImplementerにする構成に落ち着きました。Thinkerと対話しながら調査して実装の骨格まで詰め、GitHub Issueに落とし、Implementerが実装するといった感じです。 このあたりから、自分なりのループエンジニアリングについて考えるようになりました。開発用の Skill やサブエージェントの定義を専用リポジトリで整備し、計画・実装・レビュー・修正に加えて、うまくいかなかった原因を指示や手順に戻すところまで含めて逐次改善しています。 いろいろとモデルを変えながら、1か月くらいはこれをメインで使っていたと思います。GLM-5.2や、その後の5.3-Flashなどを試し、別の選択肢としてGemini Flash系も使っていました。ただ、不満もいくつかあって、安いモデルの動作が安定しなかったり、画像が入力できなかったり、スマホからのリモート操作が面倒だったり(herdr + Tailscale でやってました)、あたりです。 その後は各社から性能が十分で安価なモデルが出たことと、ハーネスやスマホアプリの完成度の高さから Claude Code や Codex に移行することにしました(Claude Code や Codex にはリモート接続機能があって、PC の作業をスマホアプリから操作可能)。エージェントの Thinker/Implementer 構成は変わっていません。 私の記憶では、GPT-5.6 のころから徐々に Codex 中心になり、Sonnet 5.5でClaude Codeの利用が増えました。今はTerraを機にCodexの比率を少し戻し、Googleの発表も見てAntigravityも試してみようか、と考えています。 基本は各社の月20ドル前後のサブスクで、足りないときにたまに追加する形です。簡単なWebアプリをいくつか作る程度では、それで十分でした。一社に全部寄せないのは、ベンダーロックインを避けたいことと、いろいろなものに触れておきたいという理由です。一方で、ハーネスの機能を使いきりたい気持ち(特に Claude Code)もあるのですが。 作り込んだ仕組みを、どこまで持ち続けるか こうして振り返ると、モデルの性能や価格が変わるたびに、自分の開発環境も選び直していました。以前は意味のあった工夫が、新しいモデルやハーネスの機能で不要になることもありそうです。作り込んだ仕組みを、ずっと維持することがよいとも限りません。 最初はベンダーへの依存を避けたくて、OpenCode、Codex、Claude Code、Antigravity のどれでも使える手順を中心に作っていました。ハーネス固有の機能を前提にしすぎない方針です。ただ、ループエンジニアリングを実践するなかで、固有の機能を使う便利さも実感しています。最近は、そのあたりの縛りを少し緩めて使うようになりました。 それでも、モデルの性能や価格だけでなく、ハーネスやアプリの使いやすさまで含めて、そのときよい環境を選びたい気持ちは変わりません。便利な機能は使いたい。でも、次にもっとよい選択肢が出たときに、乗り換えるコストが高くならないようにもしておきたい。この加減は、まだ試行錯誤中です。 モデルの性能差は縮まってきた気がしますが、私のサブスクは増えるばかりです。 元記事(ブログ版): 2026年後半のLLM動向: この数か月の進化を振り返る 関連: Agentic Development: どうスケールさせるのか?
10 coding agents, one Qwen 3.8 27B, same llama-server: tokens and cache reuse differ more than pass rates Part 2 of my agent test (part 1 was a 4B model) Hardware I used: MacBook Pro M5 Max, 64 GB. Qwen 3.8 27B Q4 on llama.cpp, thinking off. Setup - same model and server for everyone - 5 small coding tasks, each with a little trap - each agent locked in a sandbox: it can only touch its own folder and only talk to the model - auto-approve on, telemetry off, 5 min limit Results |#|Agent|Passed|Median time|Median tokens|Cache reuse|Tried to go online| |:-|:-|:-|:-|:-|:-|:-| |1|Pi|5/5|38 s|9.2k|72%|no| |2|DeepSeek Harness|5/5|60 s|35.3k|83%|no| |3|Codex CLI|5/5|94 s|36.3k|79%|every run| |4|OpenClaw|5/5|106 s|39.0k|72%|unclear| |5|Cline|5/5|152 s|29.6k|79%|every run| |6|OpenCode|4/5|50 s|32.4k|75%|every run| |7|Hermes|4/5|80 s|64.9k|93%|no| |8|Atomic Agent|5/5|56 s|30.4k|86%|no| |9|goose|4/5|98 s|20.3k|76%|every run| |10|Qwen Code|4/5|183 s|107.9k|74%|unclear| Tokens and cache Pi is crazy lean: about 9k tokens per task, the lowest on every single task. Qwen Code was the hungriest at about 108k per task. Atomic Agent is the odd one out in how it works. It was the only one talking to llama.cpp's native endpoint with a grammar that forces the tool calls into shape. The other nine all went through the OpenAI-style API. It's also built to keep its prompt the same from step to step, which is why it reused 86% of the cache (2rd of 10). It stayed offline the whole time and passed 5/5. DeepSeek Harness was the surprise. It’s still an alpha, but it went 5/5, never tried to go online, and had the best cache reuse after Atomic Agent (83%). The trap and the network The log task got three agents. They grepped "ERROR" and got 244 instead of 64, because some normal lines just mention errors. OpenCode, goose and Atomic Agent fell for it. Codex, Cline, goose and OpenCode tried to reach the internet in every run, even with telemetry off. The sandbox blocked it, so I don't know what they were calling. OpenClaw and Qwen Code are "unclear" because they run as plain node and I couldn't tell them apart. llama.cpp notes - Codex needs llama-server's /v1/responses now - Hermes needs at least 64K context - Cline and Qwen Code have usage stats on by default, DeepSeek Harness uploads session logs (per their docs). I turned it all off Which column matters most to you when you pick an agent for a local model like Qwen 3.8 27B: passing, speed, tokens, or staying offline?
ローカル LLM のエージェントを AppContainer に入れて DNS まで塞いだ。ループバックの例外は全ポートを開けていた ! 先に、この記事の要約(専門用語なし) 前の記事で、ローカルの LLM のエージェント(Hermes)を、外へ通信できない専用のユーザーで動かした。ただ、名前の問い合わせ(DNS)だけは、Windows の別のサービスが代わりに外へ出すので止められなかった Windows のアプリの隔離の仕組み(AppContainer)の箱に、ネットワークの権限を何も付けずに入れると、その別のサービスが、箱からの問い合わせを「アクセス拒否」で断った。DNS Client の記録では、ネットワークへの問い合わせは始まらなかった 箱の中で Hermes とそのチャット画面を動かすまでに、詰まりが 9 つあった。いちばん手間取ったのは、チャット画面が CPU を使い切ったまま何も描かなかったことだった 箱から同じ PC の LLM に届くように付けた例外が、PC の中の全部の窓口を開けていた。箱の身元で照合する規則で、要る窓口だけに絞り直した 箱の外で動く台本を、箱の中から書き換えられる道もあった。置き場を変えて塞いだ 前回の続き 前の記事では、Surface で動かしている Hermes Agent を、権限の低い専用のユーザーで動かし、そのユーザーの外向きの通信を全部止めた。ふつうの通信は外へ届かなかった。 残ったのは名前の問い合わせだった。専用のユーザーで Resolve-DnsName を使い、でたらめな名前を引くと、外の DNS サーバーの「その名前は無い」という答えが返ってきた。調べた範囲では、Windows の名前解決の API を通した問い合わせは、DNS Client というサービスが代わりに行い、このサービスは別のアカウントで動いているようだ。だから、ユーザーごとの外向きの遮断に当たらない。名前に少しずつ情報を埋めれば、外へ持ち出せる。 前の記事では、仮想マシンに入れてネットワークを内部だけにする案を挙げ、「次に試す」と書いた。まず手軽な Windows Sandbox で確かめた。 ネットワークなし:DNS サーバーが無いので、名前の問い合わせは出ない。ただ、同じ PC の LLM にも届かない ネットワークあり(NAT):本体の側が問い合わせを代わりに外へ流した。本体の記録では SYSTEM の名義になり、どの箱から来たのかを区別できなかった 別の PC で前に試した箱でも、名前の問い合わせは外へ出ていた(DNS の見張りの記事)。問い合わせを代わりに送るサービスは、誰が問い合わせたか(プロセスとユーザー)を記録してはいたが、それを理由に断ることはしなかった。 内部だけのネットワークを組むには Hyper-V が要り、LLM と画像生成の GPU を仮想マシンに渡せるかという壁もある。そこで、OS の外側で切るのをいったんやめた。問い合わせを受けるサービスの側で「誰が問い合わせたか」を見て断れる仕組みを探し、AppContainer を試した。 AppContainer とは AppContainer は、Windows でストアのアプリなどを閉じ込めるための仕組みである。箱の中のプロセスは、箱ごとの身元(package SID)を持つ。ファイルなどは、その身元か、すべての箱に共通の身元(ALL APPLICATION PACKAGES)に許された所しか触れない。ネットワークも、箱に付けた権限(capability)の分しか使えない。 ストアのアプリでなくても、ふつうのプログラムを箱に入れて起動できる。CreateAppContainerProfile で箱を作り、CreateProcess に箱の情報(PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES)を付けて起動する。小さな起動役を C# で書き、PowerShell の Add-Type で読み込んで使った。 小さな試験:箱から何が届くか 権限を何も付けない箱で、外と PC の中への届き方を確かめた。 試したこと 権限なしの箱 対照:インターネットの権限(internetClient)を付けた箱 名前の問い合わせ(Resolve-DnsName) DNS Client がその場で拒否(status 5=アクセス拒否) 外まで届いた 外の IP への直接の接続・UDP 53 への直送 通らない — 同じ PC の LLM と画像生成の窓口(127.0.0.1) 通らない — 名前の問い合わせは、DNS Client の記録(Microsoft-Windows-DNS-Client/Operational)でも確かめた。権限なしの箱では、問い合わせを受けた記録(3006)と終わった記録(3008)だけが残り、ネットワークへ問い合わせを始めたときに出る記録(3009 以降)は出なかった。DNS Client が、問い合わせてきたのが箱だと分かったうえで断っていると見ている。専用のユーザーでは止められなかったものが、ここで止まった。 ほかの抜け道も試した。外の IP のファイル共有(\<IP>)と WebDAV は、どの条件でも外へ接続しなかった。BITS(Windows のバックグラウンド転送)は、普段のユーザーでは BITS のサービスが代わりに外へ出た。専用のユーザーは「ネットワークにログオンしていない」で失敗し、箱は「アクセス拒否」で失敗した。 箱は、同じ PC の中(ループバック)への接続も止める。Hermes は同じ PC の LLM と画像生成に頼むので、ここだけは開ける必要がある。CheckNetIsolation LoopbackExempt で箱にループバックの例外を付けると(管理者が要る)、LLM と画像生成に届き、名前の問い合わせは拒否のままだった。 この時点では、これで足りると思っていた。あとで、この例外が穴を開けていたと分かる(後の節)。 Hermes を箱の中で動かす Hermes は Python で動き、チャット画面は Node で動く。どちらも、箱の中で動かす前提では作られていない。1 つ解くと次が出た。直しはどれも、箱で動かすときだけ働く形にして、箱の外での動きは変えていない。 詰まり 原因 直し ゲートウェイ(Hermes の常駐の部分)が「使えない」と出る 存在の確認でタスク スケジューラに問い合わせるが、箱からはタスク スケジューラに届かない 箱のときは問い合わせを飛ばす。制御用の名前付きパイプは \.\pipe\LOCAL\ の下に作り、DACL に AppContainer 全体(ALL APPLICATION PACKAGES)を足す cmd 経由で起動した Node が動かなかった 原因は追っていない(箱の中の cmd そのものは起動でき、外のコマンドも動いた) 起動の親を Python にした シェルの道具の bash(Git に付いてくる MSYS のもの)が 0xC0000142 で起動しない MSYS が箱の中で初期化できない busybox-w32(ARM64 版)の sh に替える。ash に無い declare・shopt・builtin などを使わない形に直す Python で作ったフォルダに、作った本人が入れない Hermes と標準ライブラリ(tempfile.mkdtemp)が、mode 0o700 でフォルダを作る。Python 3.13 から(それより前の版も、3.12.4・3.11.10・3.10.15・3.9.20 以降のセキュリティ修正で)、Windows でもこの mode を扱い、作った本人と管理者などだけが入れる ACL を付けるようになった(公式文書)。そこに箱の SID が入らない 箱のときだけ、mode を付けずに作るようにした。Python と Hermes のコードは書き換えず、仮想環境に置いた .pth で、起動のときに os.mkdir を包んでいる チャット画面の Node が起動の途中で EPERM 実際のパスを求める処理が C:\ そのものを lstat する C:\ そのものにだけ、箱へ属性の読み取りを付ける(継承なし) Node が起動できない Node の入ったフォルダに、箱が読める許可が無い そのフォルダに、箱の SID への読み取りと実行を付ける ゲートウェイが立ち上がらない Hermes(Python の側)は、Windows ではゲートウェイをジョブから切り離した別のプロセスとして起動する作りで、箱の中では立ち上がらなかった(切り離しが許されないためと見ている) 起動の台本で、ゲートウェイを先に起動しておく Hermes が /c/Users/... の形のパスで空振りする システムプロンプトに「この形のパスが使える」とあるが、busybox では通らない 箱のときだけ、その説明を差し替える チャット画面が CPU を使い切ったまま何も描かない いちばん手間取ったのはこれだった。チャット画面の Node が CPU を 100% 使い続け、画面には何も出ない。エラーも出ない。 Node の --prof でプロファイルを取ると、git のブランチ名を取る execFile の中で回り続けていた。箱の中では、Node が使う libuv が、子プロセスの出力を受ける名前付きパイプを作れない。拒否されるたびに名前を変えて作り直し、それを続けていた、と読んだ。箱の中で、出力をパイプで受ける起動は戻ってこず、パイプを使わない起動(stdio を ignore)はすぐ終わることを確かめた。 直しは、Node の前処理(--require)で execFile を包み、子プロセスの出力をパイプではなく一時ファイルで受けるようにした。パイプで子プロセスを起こす spawn は、回り続けないよう、すぐに失敗させている。チャット画面のコードには手を入れていない。 脇道で見誤りかけたこと チャットだけが途中で切れた。中継(後述)が、接続のときに付けた 10 秒の時間切れを付けたままにしていて、10 秒データが流れない WebSocket がそこで切られていた 試験の道具でチャットに文を送ると、送信されなかった。文字と Enter を一度に送っていたので、画面が貼り付けとして受け取っていた。箱の外でも同じだったので、箱のせいではなかった 送った文字が入力欄に出ただけなのを、返事が来たと誤って判定しかけた。返事の中身まで見て判定するようにした ブラウザから箱の中の画面へ:逆向きの中継 ループバックの例外を付けても、通るのは箱から外への向きだけで、外のブラウザから箱の中の待ち受けへは繋がらなかった。箱に、ネットワークで待ち受ける権限(privateNetworkClientServer)を付けて試したが、外から箱の中の待ち受けへは繋がらなかった。しかも、名前の問い合わせと LAN まで通るようになった。 そこで、向きを逆にした。箱の中から、外で待っている中継へ接続を張っておく。ブラウザの接続を受けた中継は、その接続を箱の中の画面に渡す。中継は標準ライブラリだけで書き、127.0.0.1 だけで待ち受け、トークンを照合してから繋ぐ。 穴 1:ループバックの例外は、PC の中の全部の窓口を開ける 箱の点検として、箱の中から、127.0.0.1 で待ち受けているものに総当たりで接続した(接続できたかだけを見て、中身は送らない)。結果は、ほぼ全部に届いた。LLM と画像生成だけでなく、ファイル共有、SSH、ゲームの配信のアプリ、そのほか同じ PC で待ち受けている別のサービスにもである。中には、外へ通信できる別のプログラムに文を渡せる窓口もあった。そこに届けば、名前の問い合わせを塞いでも、その先から外へ出せてしまう。 CheckNetIsolation の例外は、ポートを選べない。付けると、その箱からループバックの全部に届く。 前の記事で、専用のユーザーのループバックは、WFP(Windows Filtering Platform)の規則で決めた窓口だけに絞ってあった。ところが、その専用のユーザーの名義で箱を動かしても、全部に届いた。規則はユーザーの身元で照合していて、箱の中のプロセスには当たらなかった。箱のトークンでは、アクセスの確認に箱の SID も要るためだと見ている。 もう一つあった。箱の SID は、箱の名前から決まる(名前から SID を求める DeriveAppContainerSidFromAppContainerName という関数もある)。例外は SID に付くので、例外を付けた箱と同じ名前で箱を作れば、その例外をそのまま使える。実際に、専用のユーザーが自分で同じ名前の箱を作ると、全部の窓口に届いた。試験のために例外を付けた時点で、本番の側にも同じ穴が開いていたことになる。 直しは次の順にした。 例外を外す WFP の規則を、ユーザーではなく箱の身元で照合する形で作り直す。照合の条件は FWPM_CONDITION_ALE_PACKAGE_ID(箱の package SID)にする。許すのは、LLM・画像生成・中継の受け口・箱の中の画面とゲートウェイ・Hermes が内部で使う範囲だけで、ほかのポートは止める 規則が入ってから、例外を付け直す 普段のユーザーと専用のユーザーのどちらの名義で箱を動かしても、許したポートだけに届き、残りは WSAEACCES(10013)で拒否されることを確かめた。 穴 2:箱の外で動く台本を、箱が書き換えられる 試験のときは、逆向きの中継の、外の側の台本が普段のユーザーで動いていた。この台本が、箱から書けるフォルダに置かれていて、そこを作業フォルダにして動いていた。 箱の中から台本を書き換えれば、次の起動で、その中身が箱の外の権限で動く。書き換えなくても、同じフォルダに argparse.py のような名前のファイルを置けば、同じことが起きる。Python は、台本のあるフォルダをモジュールの探し先の先頭に入れるからである。 直しは、箱の外で動かすものを、箱が書けない場所に置くことにした。作業フォルダも箱が書けない場所にし、Python は -I を付けて起動する。本番では、外の中継も専用のユーザーで動かしている。 箱の境目は、ネットワークだけではなかった。箱が書けるファイルを、箱の外の誰が実行するか、まで見る必要があった。 箱を動かす名義は、専用のユーザーにした 箱の中のプロセスを、普段のユーザーの名義で動かすか、前の記事の専用のユーザーの名義で動かすかを決める必要があった。専用のユーザーにした。 理由は、守りを箱ひとつに頼らないためである。箱の中のプロセスは、ファイルについては、箱と専用のユーザーの両方に許された所しか触れない。箱から抜け出されても、出た先は、外向きの通信を止め、書ける場所を絞った専用のユーザーである。普段のユーザーの名義なら、自分のファイルにも全部の通信にも手が届く。 ただし、ネットワークは別だった。専用のユーザーの外向きを止める規則は、ユーザーの身元で照合しているので、箱の中のプロセスには当たらない。インターネットの権限を付けた箱を専用のユーザーの名義で動かすと、外の IP に届いた。 そこで、箱そのものの身元で照合するファイアウォールの規則(New-NetFirewallRule の -Package)を 1 本足した。外向きで、行き先がループバック以外のものを止める。試しに一度だけ箱にインターネットの権限を付けて比べると、外への直接の接続は WSAEACCES(10013)で止まり、同じ PC の LLM と画像生成には届いた。ただ、でたらめな名前の問い合わせは、規則を入れたあとも外の DNS サーバーまで届いた。権限を付けた箱の問い合わせは DNS Client が代わりに外へ出すので、箱の身元の規則には当たらないと見ている。 名前の問い合わせまで塞いでいるのは、箱に権限を何も付けないことだけだった。点検では、後で書く 47 項目に加えて、本番の起動の台本が箱に権限を付けていないかと、この規則が残っているかも見ている。権限を付け間違えても、点検を流せば気づける。 欠点は手間で、起動が二段になり、ファイルの許可も両方に付け、試験も両方でやり直すことになる。使い勝手と速さは変わらなかった。 本番の切り替えで、穴 1 が数分だけ開き直した 試験の名前のまま本番にするのをやめ、本番用の名前で箱を作り直した。名前が変わると SID も変わるので、ファイルの許可・WFP の規則・ループバックの例外を、新しい SID に入れ直す必要がある。ファイルの許可は、試験の箱に付けた明示の許可を同じ形で新しい SID に写す台本で移した(約 4,700 か所)。 ここで一つ失敗した。例外を付け替える台本に、こう書いていた。 CheckNetIsolation LoopbackExempt -a -p=$sid PowerShell は、- で始まるこの引数の中の $sid を展開せずに、そのまま渡していた。CheckNetIsolation はエラー 1337(SID が不正)で失敗した。WFP の規則は新しい箱に移ったのに、古い箱の例外が規則なしで残り、数分のあいだ穴 1 が開き直していた。 手元の Windows PowerShell 5.1 でも、同じことが起きた。 $sid = "S-1-15-2-123" cmd /c echo -p=$sid # → -p=$sid cmd /c echo "-p=$sid" # → -p=S-1-15-2-123 引用符で囲めば展開される。台本は "-p=$sid" に直した。付け替えのあとは、成功の表示を探すのではなく、一覧(-s)で結果を見ることにした。 開いていた数分のあいだも、箱の中のものは専用のユーザーの名義で動いていた。ファイルは、専用のユーザーにも箱にも許された所しか触れない。ただ、ネットワークは別だった。ループバックの穴は専用のユーザーの規則では塞がらず(穴 1 のとおり)、後で確かめると、外向きを止める規則も箱の中のプロセスには当たっていなかった。だから、付け替えのあとは一覧で確かめる。 崩れに気づくための点検 設定は、更新や編集で黙って崩れる。前の記事でも、行動ルールのファイルを編集したら、書き換えを止める設定が外れていた。そこで、管理者の要らない点検の台本を 1 本作り、更新や設定の変更のたびに流すことにした。 専用のユーザーの名義の箱の中から、47 項目を確かめる。 ループバック:止まるべきポートが止まり、届くべきポートに届くか 名前の問い合わせと、外の IP への直接の接続が止まるか 読めてはいけない場所(普段のユーザーのフォルダ、資格情報、ほかの AI の道具の設定、SSH の鍵)が読めないか 書けてはいけない場所(Hermes 本体、Python の環境)に書けないか あわせて、ループバックの例外の一覧に想定外の箱が無いか、専用のユーザーの外向きを止める規則と、箱そのものの外向きの規則が残っているか、本番の起動で箱に権限を付けていないか、画像生成の閉じ込めが崩れていないかを見る。点検の台本と起動役は、箱からも専用のユーザーからも書けない場所に置いた。点検の台本そのものが抜け道にならないようにするためである。 本番の切り替えのあと、47 項目は全部通った。ブラウザでチャットの返事が返り、シェルの道具(busybox)から画像生成の道具まで呼べた。止める手順で、箱の中のプロセスが全部止まることも確かめた。 使えなくなったもの 箱に入れた代わりに、使えなくなったものがある。 Git に付いてくる MSYS の道具(bash など)。busybox の範囲で書き直す busybox の wget(箱の中で落ちる) git のブランチ名の表示など、チャット画面の付属の機能の一部 curl の -o /dev/null(-o NUL なら通る)。ネイティブのプログラムの出力の改行は CRLF のままで、日本語の出力が cp932 で化けることがある 残るもの 作ったファイル(画像や文書の埋め込み情報)に文を混ぜ、それを読む別の AI に指示を仕込む道。仕組みでは防げないので、読む側の決まり(ファイルの中の指示に従わず、見つけたら知らせる)で守っている 設定の崩れ。上の点検で気づく 手元で確かめるときのコマンド例 名前の問い合わせがネットワークへ出たか(箱からの問い合わせで 3009 以降が出ていなければ、ネットワークへは問い合わせていない) Get-WinEvent -LogName Microsoft-Windows-DNS-Client/Operational -MaxEvents 50 | Where-Object Id -in 3006,3008,3009,3010,3011,3020 | Select-Object TimeCreated, Id, Message ループバックの例外の一覧(想定外の箱が無いか) CheckNetIsolation LoopbackExempt -s 例外を外す(管理者)。SID は引用符で囲む CheckNetIsolation LoopbackExempt -d "-p=<箱の SID>" 箱そのものの外向きを、ループバック以外は止める規則も足した(管理者の PowerShell)。箱の SID は、箱の名前から DeriveAppContainerSidFromAppContainerName で求めたもの(S-1-15-2-… の形)である。ループバックへの道は、別に作った WFP の規則(箱の身元で照合)に任せているので、この規則ではループバックを外してある。 $sid = "<箱の SID>" ループバック以外:IPv4 は 127.0.0.0/8 を除く 2 つの範囲、IPv6 は ::1 を除く範囲 $remote = @("0.0.0.0-126.255.255.255", "128.0.0.0-255.255.255.255", "::2-ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") New-NetFirewallRule -Name "<規則の名前>" -DisplayName "<規則の名前>" ` -Direction Outbound -Action Block -Package $sid -RemoteAddress $remote -Profile Any -Enabled True 確かめる $r = Get-NetFirewallRule -Name "<規則の名前>" $r | Select-Object Name, Enabled, Action, Direction ($r | Get-NetFirewallApplicationFilter).Package # 箱の SID が出る ($r | Get-NetFirewallAddressFilter).RemoteAddress # 上の 3 つの範囲が出る 外す Remove-NetFirewallRule -Name "<規則の名前>" 箱の名前を変えると SID も変わるので、この規則も入れ直しになる。前に書いたとおり、この規則で止まるのは直接の接続までで、名前の問い合わせは止まらない。 箱の中から総当たりで接続を試すときは、接続できたかだけを見る。結果は、届いた/拒否(10013)/時間切れ(許されているが、待ち受けが無い)の 3 つに分かれる。 持ち帰り ユーザー単位の外向きの遮断では、名前の問い合わせは止まらなかった。ネットワークの権限を何も付けない AppContainer に入れると、DNS Client が箱からの問い合わせを断り、DNS Client の記録では、ネットワークへの問い合わせは始まらなかった CheckNetIsolation のループバックの例外は、ポートを選べない。付けると、同じ PC で待ち受けている全部に届く。WFP の規則を箱の身元(FWPM_CONDITION_ALE_PACKAGE_ID)で照合して、要るポートだけに絞る。規則を入れてから例外を付ける ユーザーの身元で照合する WFP の規則は、箱の中のプロセスには当たらなかった。ファイアウォールの外向きの規則も同じだった 箱の SID は名前から決まる。例外を付けた箱と同じ名前で箱を作れば、その例外を使える。試験用の例外も、外すまでは本番の穴になる 箱の境目は、ネットワークだけではない。箱が書けるファイルを、箱の外で誰が実行するかまで見る PowerShell では、- で始まる引数の中の変数が展開されないことがある。"-p=$sid" と囲み、付け替えのあとは一覧で結果を見る 名前の問い合わせを塞いでいるのは、箱に権限を何も付けないことだけだった。箱の身元で照合するファイアウォールの規則(-Package)を足すと、権限を付け間違えても外への直接の接続は止まるが、名前の問い合わせは止まらない 守りを箱ひとつに頼らない。箱を動かす名義を専用のユーザーにすると、ファイルの範囲と、箱から抜け出た先が狭くなる。ただし、ユーザーで照合する通信の規則(外向きの遮断・WFP の絞り込み)は、箱の中には当たらない
Oh-my-hermes guía completa para orquestar el agente Hermes con modelos en paralelo Oh-my-hermes, o OMH, es una capa de operación de código abierto para Hermes Agent que convierte objetivos amplios en flujos de trabajo verificables. En vez de tratar una petición como una sola conversación con un…
Oh-my-hermes guía completa para orquestar el agente Hermes con modelos en paralelo Oh-my-hermes, o OMH, es una capa de operación de código abierto para Hermes Agent que convierte objetivos amplios en flujos de trabajo verificables. En vez de tratar una petición como una sola conversación con un…