Skip to content

Harden subagent safety boundaries: symlink rejection, bounded reads, worker loop hardening (continuation of PR #1)#2

Draft
bgoertzel-sing wants to merge 132 commits into
hlgreenblatt:mainfrom
bgoertzel-sing:agent/threadkeeper-hardening-next
Draft

Harden subagent safety boundaries: symlink rejection, bounded reads, worker loop hardening (continuation of PR #1)#2
bgoertzel-sing wants to merge 132 commits into
hlgreenblatt:mainfrom
bgoertzel-sing:agent/threadkeeper-hardening-next

Conversation

@bgoertzel-sing

Copy link
Copy Markdown

Summary

This is a large hardening continuation of the ThreadKeeper subagent safety work. It builds on the foundations in draft PR #1 (agent/threadkeeper-safety-floor) and adds 112 additional commits on top of that branch.

Key changes (123 commits total vs main)

Symlink rejection across all file-bearing paths:

  • Reject symlinks for persona setup, audit reads/writes, transcript audit records, LLM guard state, run index files, integrity sidecars, queued task records, async worker locks, workspace file locks, and escalation policies.
  • Uses no-follow regular-file opens where available.

Bounded reads for all untrusted/external file content:

  • Configurable byte caps for worker LLM state, persona setup, escalation policy, candidate review sidecars, checksum sidecars, transcript hash audits, run index append tails, worker lock metadata, and shell output memory capture.
  • Oversized-file errors no longer echo absolute local paths.

Worker loop safety:

  • Graceful SIGTERM/SIGINT handling with clean stop reason.
  • Consecutive-error cap to stop cycling workers.
  • Async worker signal state cleared after run.
  • Dispatch-level wall-clock timeout (OMEGACLAW_SUBAGENT_DISPATCH_TIMEOUT_S).
  • Wall-clock timeout checked before each LLM call and tool execution.
  • Worker usage accounting writes hardened.

Subagent call hardening:

  • Task contract validation (inline and file-based).
  • String-only final emits and tool arguments.
  • Worker control token paths confined.
  • Patch proposal transcript content bounded.
  • LLM concurrency and rate-limit guards.
  • Persona integrity pinning and structured setup error recording.
  • Atomic append writes for subagent outputs.
  • Transcript checksums for tamper detection.

Budget-aware orchestration:

  • threadkeeper_budget.py: configurable hybrid OmegaClaw mesh for budget-aware orchestration.
  • Escalation policy lifted from Python into MeTTa (Atomspace rules).
  • should_escalate wired into the live delegate path with cloud-gating.

Verification

  • 186 tests passed (latest run with mock pytest gate)
  • py_compile clean
  • git diff --check clean

Relationship to PR #1

PR #1 (agent/threadkeeper-safety-floor) established the initial safety floor. This branch (agent/threadkeeper-hardening-next) supersedes and extends it with 112 additional commits. If preferred, this PR can replace PR #1.

Notes

  • This is a draft PR for review. The commit count is large; squash-merge or rebase-merge is recommended.
  • No breaking changes to the existing ThreadKeeper dashboard or CLI are intended.
  • All tests are backward-compatible with the existing mock pytest harness.

hlgreenblatt and others added 30 commits June 17, 2026 15:31
…re orchestration

ThreadKeeper decouples reasoning quality from reasoning frequency: a cheap
persistent local control loop holds the thread, a local worker loop iterates
cheaply, and cloud specialists are invoked only for hard subproblems under an
explicit token budget. Built as an extension to OmegaClaw-Core (synced to
current upstream main), changing none of its existing behavior.

Adds:
- src/subagent.py + delegate skill (skills.metta): bounded, governed delegation
  primitive — the "cloud specialist"/"worker" node mechanism.
- src/threadkeeper_budget.py: cost-awareness seam — per-loop token accounting
  and an escalation decision against a configurable budget; auditable trail
  (memory/usage.jsonl + memory/escalations.jsonl), ISO/IEC 42001-friendly.
- threadkeeper.config.yaml: the four-node mesh + budget threshold in one place;
  models are clearly-labelled examples, every role swappable, keys by env-var
  name only (no secrets).
- docs/architecture.md (+ reproducible architecture.png), docs/subagent-design.md,
  reference-skills-subagent.md, tutorial-09-subagents.md.
- memory/personas-subagent/ scaffolding with researcher.json.example; real
  configs and runtime logs gitignored.
- README rewrite (pitch -> problem -> four-node solution -> "improvement to
  OmegaClaw" as extension) and HACKATHON.md (BGI Sprint I, team ThreadKeeper).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The working hackathon demo, ported from runtime into the repo so it reproduces:

- channels/local.py: the WebUI dashboard — a live 4-engine mesh (control /
  local-worker / cloud-specialist-A / cloud-specialist-B) showing per-engine
  token + cost, a counterfactual "all-frontier vs ThreadKeeper" savings
  headline, an "OmegaClaw DNA" panel proving the MeTTa/Hyperon substrate is
  present + intact, and a live reasoning ("thinking") pane tailing history.metta.
  Plus the agent avatar route.

- src/subagent.py: worker dispatch hardened — LOCAL Ollama workers called via
  the native /api/chat path with think:false (reasoning models return empty
  content via /v1 on current Ollama builds); CLOUD workers via /v1. Worker
  token usage now logged to memory/usage.jsonl so delegated work shows on the
  mesh's Local Worker tile. No longer depends on the AIProvider class.

- demo/: bulk document-summarization demo — chunks a doc and routes each chunk
  to the local granite worker (high token volume, trivial reasoning, $0). The
  doc itself explains ThreadKeeper's thesis, so the summaries reinforce the pitch.

- memory/personas-subagent/: example worker (local granite) + specialist
  (GLM 5.2 cloud) persona configs. Keys by env-var name only; endpoints
  genericized to localhost.

- PITCH.md: the 3-minute pitch script (problem -> mesh -> live demo -> savings).
- docs/disaster-recovery-and-migration.md: memory persistence + cross-provider
  migration analysis (the embedding-provider trap; how learned memory stays
  portable under ThreadKeeper).

No secrets committed: all credentials referenced by env-var name only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- docs/recursive-self-improvement.md + docs/xi-interview.md: a session where the
  agent (隙) diagnosed its own pin-spam failure, designed the fix (loop-detection
  governor), had it built to spec in the channel wrapper it insisted on (outside
  its own control), and watched its own loop quiet — ~183 repetitions → 1 clean
  message. Plus: it re-derived ThreadKeeper's thesis, critiqued its own free-cloud
  control loop, and had its hardware pick (Qwen2.5-14B, not 9B) experimentally
  confirmed. OmegaClaw's stated RSI goal, demonstrated, with a human-in-the-loop
  governor put first by the agent's own design. README pointer added.

- HACKATHON.md: soften "synced to current upstream" → "branched from a recent
  upstream main (June 2026)" — accurate (the fork is ~36 commits behind current
  upstream; this avoids an overclaim a reviewer could flag).

No secrets: all credentials by env-var name only; transcript scanned clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… $0, hard->GLM+DeepSeek). Shows 'escalate only when needed' with all tiers, fractions of a cent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-min pitch

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Reasoning-engines dashboard now COLLAPSED by default (click to expand) so the
  chat is the star — for non-technical users who just want to talk to the agent.
- Token counters are now ALL-TIME by default (append-only usage.jsonl): tiles
  only climb, survive restarts, never reset/lose data.
- Honest savings: control loop (MiniMax) counts as cloud, not 'free' — shows
  on-prem%/cloud%/paid%; savings compared at NORMAL (non-promo) rates; the
  hackathon free tier noted separately.
- 5-tile Local/Cloud column layout (Granite + Qwen local workers shown distinctly
  for reasoning diversity); full-width fills the screen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-history ledger

- Primary Reasoning Engine tile is now DYNAMIC: reads PRIMARY_LLM_MODEL from
  the live container env, so it shows whatever holds the thread right now
  (Qwen2.5-14B today; flips automatically when the control loop is swapped).
- LOCAL column now reflects CURRENT configs (Qwen2.5-14B on .248, Gemma 4 12B
  on .41) instead of stale historical worker labels.
- Full model history moved into its own collapsible '📜 Full history' menu
  below the current-config dashboard: every model ever run, all-time totals,
  'created N days ago', current primary row highlighted.
- All-time ledger sourced from the append-only usage.jsonl (counts never reset).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ThreadKeeper's core decision — whether to escalate to a cloud specialist —
now lives in src/escalation.metta as MeTTa pattern-matching rules (tk-escalate),
evaluated through OmegaClaw's own MeTTa runtime (PeTTa). threadkeeper_budget.py
becomes the seam: it supplies live facts (spend, ceiling, soft threshold, local
iterations, hard-flag) and executes the verdict, but the routing logic is now
symbolic and agent-readable/-rewritable via the existing read-file/write-file
skills — the same self-modification property OpenCog Hyperon is built around.

- src/escalation.metta: the 4-rule policy as Atomspace rules; numbers stay in
  threadkeeper.config.yaml (the .metta owns logic only).
- threadkeeper_budget.py: _MettaPolicy loads the policy into PeTTa once and
  evaluates it MeTTa-first; on any failure (host/CI without the runtime) it
  falls back to the identical Python rules, preserving the never-raises contract.
  Hardened PeTTa import to self-discover <PeTTa>/python on sys.path.
- tests/test_escalation_metta_parity.py: proves the MeTTa verdict equals the
  Python spec across every branch + boundary (64/64), all served via [metta];
  skips cleanly where PeTTa is absent.
- README/HACKATHON: 'policy lives in MeTTa, agent-rewritable' — the honest
  answer to 'it's just Python orchestration.'

Behavior is identical (parity-verified); this is an architecture/transparency
improvement, not a policy change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The (delegate ...) skill now consults ThreadKeeper's MeTTa budget policy
before dispatching to a CLOUD specialist. A denied delegation is refused and
returns the [metta]-tagged reason to the parent loop instead of spending; LOCAL
delegations (Ollama on .41/.248) are free and pass through ungated. The gate
fails OPEN — if the policy can't be evaluated, the call proceeds — so a broken
governor never silently halts the agent.

subagent.py:
- _persona_is_cloud(): classify a delegation as cloud vs local (explicit
  node_role, else base_url heuristic — the same one _call_subagent_llm uses).
- _escalation_gate(): for cloud, construct a BudgetTracker and call
  should_escalate(hard=True); returns (allowed, reason). Self-discovers the
  budget module beside the overlay or in repo src/. Never raises.
- dispatch(): consult the gate right after persona load; on deny, return the
  refusal digest before resolving the provider (saves the spend).

threadkeeper_budget.py — two fixes the live wiring exposed (without them the
gate would deny forever on the real agent log):
- _iter_records: records written WITHOUT a thread_id (the worker/loop usage log
  format + the dashboard's own records) now count toward the default thread,
  instead of being filtered out (which made spent_tokens always 0).
- local_iterations: records without an explicit node_role are classified
  local-vs-cloud by model name, so the real usage log (which omits node_role)
  counts cheap iterations correctly.

Verified live: a real dispatch() to the cloud 'specialist' persona is gated by
the Atomspace policy and the verdict lands in memory/escalations.jsonl;
local delegations pass ungated. Parity test still 64/64.

Deployment: agent_10 relaunched via the new launch-agent-10.sh (mounts
escalation.metta + threadkeeper_budget.py into the agent's src/); env + run
spec captured for reboot-survival.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clarify that the mesh dashboard + budget tracker read ONE agent's usage.jsonl:
per-agent (not fleet-wide), tokens dominated by re-sent loop context, costs at
example rates, savings is a counterfactual. The dashboard answers 'what did this
agent route where, at illustrative rates' — the architecture story, not a
billing console.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ben goertzel added 30 commits July 8, 2026 02:05
_tool_read_file and _tool_append_file used plain open() after
_resolve_workspace_path (which uses realpath for containment).  Add
_open_workspace_file_read helper that opens with O_NOFOLLOW and
validates the fd is a regular non-symlink file, closing a TOCTOU
symlink-swap gap between path resolution and the actual file read.
Add focused tests for the helper, symlink-escape rejection, and
regular-file acceptance.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant