Skip to content

Joysafeter v2#141

Open
yuzzjj wants to merge 575 commits into
mainfrom
joysafeter-v2
Open

Joysafeter v2#141
yuzzjj wants to merge 575 commits into
mainfrom
joysafeter-v2

Conversation

@yuzzjj

@yuzzjj yuzzjj commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

yuzzjj and others added 30 commits April 27, 2026 13:03
…dundant workspace file services across backend and frontend
…ill permissions, skill version, task activity, task, thread, workspace services, and utility functions. These deletions streamline the test suite by eliminating outdated or redundant tests, ensuring a more maintainable codebase.
…ect documentation and focus on finalized design specifications.
- Removed the draft boundary design document and the draft to initial version terminology design document as they are no longer needed.
- Updated the login form to improve error handling for authentication errors, utilizing a centralized error message mapping.
- Enhanced OAuth button component to handle errors more effectively, including specific error messages for various OAuth-related issues.
- Improved Copilot error boundary to differentiate between network and WebSocket errors using error codes.
- Refactored useCopilotActions hook to streamline error handling and improve user feedback for authentication and network errors.
- Updated English and Chinese localization files to include new error messages related to OAuth provider issues.
…services and components

- Reordered import statements for consistency in `execution_service.py`, `mcp_server_service.py`, `oauth_service.py`, `organization_service.py`, `platform_token_service.py`, `sandbox_manager.py`, `skill_collaborator_service.py`, `skill_service.py`, `skill_version_service.py`, `task_activity_service.py`, `task_service.py`, `thread_service.py`, `workspace_service.py`, and `execution_subscription_handler.py`.
- Consolidated error messages in `organization_service.py`, `skill_collaborator_service.py`, `task_activity_service.py`, and others for better clarity and maintainability.
- Updated error handling to use `createApiError` in various frontend services and hooks for consistent error structure.
- Added tests for `use-test-model-stream` and `api-client` to ensure proper error handling and response validation.
- Removed unnecessary blank lines and improved formatting for better readability.
… structured error payloads and specific error codes
- Updated InternalServiceError raising in live_read_file to include detailed error context.
- Introduced normalize_app_error function to standardize error normalization across the application.
- Refactored normalize_exception to utilize normalize_app_error for better consistency.
- Added error_payload fields in CLIResult for ClaudeCodeProvider and CodexProvider to provide structured error information.
- Enhanced ExecutionRunner to return structured error payloads using normalize_app_error.
- Updated OpenClawProvider to handle error payloads and emit structured error messages.
- Modified GraphEngine and CopilotEngine to utilize normalize_app_error for error handling.
- Improved error handling in orchestrator methods to include specific error codes and data.
- Added tests for structured error payloads in CLIMessage and OpenClawProvider.
- Updated frontend types to accommodate structured error payloads in error handling.
…ion-specific error classes across core services and engine components
Designs a Langfuse-aligned observation system for in-product agent debugging:
unified ObservationCollector across all four engines (Graph/CLI/Code/Copilot)
with WebSocket streaming of structured trace trees to the Builder UI.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
- Rename Trace.metadata/Observation.metadata attribute to meta to avoid
  SQLAlchemy Base.metadata collision (column name unchanged)
- Clarify collector.flush delegates to writer.flush, distinguish from finalize
- Make broadcaster seq counter explicit
- Document finalize behavior preserves ERROR level over auto-closed WARNING
- Scope span_update to v1 Copilot streaming only
- Note v1 Copilot ships without cost calculation
- Gate trace_context.py deletion on external caller check

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
16-task TDD plan covering Phase 1 (core layer: types/model/writer/broadcaster/collector),
Phase 2 (engine integration: all 4 engines + orchestrator + instrumentation),
Phase 3 (API + frontend outline).

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
…aligned with Langfuse

Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
Introduces Trace and Observation ORM models aligned with the Langfuse
schema. Uses `meta` as the Python attribute name mapped to a `metadata`
column to avoid collision with SQLAlchemy's Base.metadata. All required
fields are NOT NULL; server_default values are set for status and level.
14/14 tests passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e files

- Simplified object key handling in agent-diff.ts
- Enhanced formatting and readability in errors.ts
- Improved filter logic in filters.ts
- Streamlined ID stripping logic in id.ts
- Refined role options function in roles.ts
- Consolidated secret key definitions in secret-keys.ts
- Enhanced draft zip file naming in skill-draft-zip.ts
- Cleaned up skill import utilities for better clarity
- Optimized skill import handling in skill-import.ts
- Improved skill version diffing logic in skill-version-diff.ts
- Enhanced SSE handling in sse.ts
- Refined URL trimming logic in url-trim.ts
- Streamlined permissions provider rendering in permissions-provider.tsx
- Improved proxy response handling in proxy.ts
- Simplified agent type definitions in agent.ts
- Refined managed types for better clarity and consistency in managed.ts
@yuzzjj yuzzjj requested a review from GLei16 July 2, 2026 13:01
…ing)

Closes the residual zombie race left by the running-task lease: an instance
that stalls without crashing (GC pause / partition) can wake after its lease
lapsed and its task was reclaimed + re-run under a new owner, then make a stale
mutating write that corrupts the task the new owner now runs.

Each →RUNNING claim stamps a globally-monotonic owner_epoch (a Postgres
SEQUENCE — the only durable monotonic source; Redis has none and a task-local
counter can't survive a cross-instance requeue). Mutating writes for a running
task (status transition, error, output, usage) and failover_or_fail_task are
conditioned on the epoch the caller was granted; a stale-epoch write matches
zero rows and is dropped. The token is threaded through both terminal-write
paths — the in-process TaskRunner and the gRPC completion handlers
(_handle_result / _handle_reconnect_result via bridge.current_owner_epoch).
expected_epoch=None preserves the prior status-only behavior for callers that
hold no grant (pre-RUNNING scheduler/watchdog).

Independently verified: 28/28 tests green against real Postgres (7 new fencing
tests incl. end-to-end stale-write and stale-failover drops), ruff + format
clean, mypy clean on the changed files, alembic chain linear with reversible
up/down.

Co-Authored-By: Claude-Opus-4.8[1m] <noreply@anthropic.com>
@yuzzjj yuzzjj requested a review from GLei16 July 2, 2026 13:03
GLei16
GLei16 previously approved these changes Jul 2, 2026
yuzzjj and others added 19 commits July 3, 2026 08:59
…reads

Cross-instance input (a tool confirmation / interrupt that lands on an
orchestrator instance which is NOT the sandbox owner, routed to the owner via
Redis joysafeter:cmd:{owner}) was silently dropped. CommandListener._dispatch
enqueued the input into bridge.runner_tx — an asyncio.Queue with no consumer
anywhere in the repo — and logged success, but the runner loops deliver input
by draining bridge._control_queue on confirmation, so the agent resumed from
its HITL wait with no input. Remote cancel only worked incidentally because it
also set _cancel_event.

Route input via bridge.send_control_input(content) (-> _control_queue +
confirmation_event, matching the local path) and cancel via
bridge.request_cancel() (-> _cancel_event). This fixes both the gRPC-runner and
in-process TaskRunner execution modes, since both consume _control_queue /
_cancel_event, not runner_tx.

Independently verified PASS: 31/31 tests green (3 new routing tests, RED-proven),
ruff/format/mypy clean; whole-repo grep confirms runner_tx has no consumer.

Co-Authored-By: Claude-Opus-4.8[1m] <noreply@anthropic.com>
…stream

grpc.aio forbids concurrent writes to one stream, but the orchestrator wrote to
a sandbox's runner stream from many coroutines (the gRPC handler loop plus
API/relay/shutdown paths) — a latent interleave/corruption hazard. Separately a
`bridge.runner_tx` asyncio.Queue had no consumer, so writes enqueued there were
silently dropped (graceful shutdown, agent-cancel, memory-sync).

SandboxBridge.write_to_runner(msg) is now the ONE way to send to the runner: it
holds a per-bridge asyncio.Lock so writes can never overlap, re-checks the
stream for None under the lock (returns False/drops instead of raising on a
concurrent disconnect), and every producer — the ~10 gRPC handler-loop writes
and all external writers — funnels through it. The dead runner_tx queue and the
send_to_runner shim are removed. Because it's a direct locked write (not a
task-loop-dependent drain), shutdown/memory-sync/cancel now reach even
idle-but-connected sandboxes.

Independently verified PASS: 34/34 tests green (incl. a serialization test and
adversarial probes — 200 concurrent writers stay max_in_flight==1, and a
disconnect that nulls the stream mid-flight is caught by the under-lock
re-check), ruff/format/mypy clean, grep-confirmed sole writer (zero
context.write / runner_stream.write / runner_tx references remain).

Co-Authored-By: Claude-Opus-4.8[1m] <noreply@anthropic.com>
…ion 3)

A single tenant (project) could occupy unbounded orchestrator/sandbox capacity
and starve every other tenant on the shared HA fleet. Gate task submission on
the project's live (non-terminal) task count.

- Project.max_concurrent_tasks (nullable) overrides the global
  max_concurrent_per_project default; migration 20260703_000005.
- TaskService.count_active_tasks_for_project + resolve_project_task_limit hold
  the load-bearing logic (Postgres-tested); the API is thin glue.
- create_task returns a structured 429 (code/limit/active/project_id +
  Retry-After) placed AFTER the idempotency short-circuit, so an idempotent
  retry is never rejected.
- Soft limit: count-then-create can slightly over-admit under concurrent
  submits — acceptable for a fairness quota (idempotency stays strict).

Independently verified PASS: ruff/mypy clean, 40 tests pass (incl. 6 new +
fresh-container migration chain), single alembic head.

Co-Authored-By: Claude-Opus-4.8[1m] <noreply@anthropic.com>
sandbox_cpu/sandbox_memory_mb were defined but never passed to the provider,
so every Docker sandbox ran unbounded — one tenant's agent could exhaust host
CPU/RAM on the shared fleet. Wire the limits through and enforce safe defaults.

- Defaults now enforced: sandbox_cpu=2.0 cores, sandbox_memory_mb=4096 MB
  (still Optional; None disables). Overridable per-deployment via env.
- Per-project override: nullable Project.max_cpu / max_memory_mb columns;
  migration 20260703_000006. Resolved per-field (a project may override only
  one); NULL falls back to the global default.
- resolve_project_sandbox_limits (Postgres-tested) holds the decision; the
  Docker provider translates it to HostConfig NanoCpus / Memory. A
  project-agnostic warm-pool sandbox uses global defaults with no DB read.

Independently verified PASS: ruff/format/mypy/pyright clean, 47 tests pass
(incl. 7 new + fresh-container migration chain), single alembic head. Adversarial
probe confirmed a 0.0 override is honored (is-not-None, not truthiness).

Co-Authored-By: Claude-Opus-4.8[1m] <noreply@anthropic.com>
…undation 2)

Make session-status events durable and observable instead of best-effort:
- Batch writer no longer silently drops on a wedged queue; it writes the
  event synchronously and records a lost-event counter for /health.
- Session status events are routed through the persist-then-broadcast path
  and skipped by the async batch/stream persisters to avoid double-writes.
- EventBus and EventBatchSender expose health_snapshot(); /health surfaces
  event_bus / event_buffer / event_stream backlog + dead-letter state.

Co-Authored-By: Claude-Opus-4.8[1m] <noreply@anthropic.com>
…depend on them

Prevent pulling dependencies out from under a running task (which for
pentest workloads means irreproducible, externally-visible side effects):
- agent: reject secret_ref/environment_ref change, delete, archive, and
  session-archive while the agent has active tasks; validate environment_ref
  exists and is not archived on create/update.
- environment / secret / memory-store: reject update (name/config), delete,
  and archive while referenced by an active task, agent, or live session.
- project: reject archive while it has active tasks.
Endpoints translate the new service-layer ValueErrors into 409/503.

Co-Authored-By: Claude-Opus-4.8[1m] <noreply@anthropic.com>
…asks (Foundation 3)

Add nullable, non-FK-constrained user_id/org_id columns to joysafeter_tasks
for attribution/audit and per-user admission control (survive user/org
deletion). Composite index (user_id, status) backs the per-user active-task
count. Schema only; the writer and admission gate land in later commits.

Co-Authored-By: Claude-Opus-4.8[1m] <noreply@anthropic.com>
…re (Foundation 1)

Make session-state ownership explicit so a stale task can no longer flip a
session that a newer task now owns, and stop stale sandboxes from writing
terminal state after failover:
- New SessionService.update_session_status_for_task_event(session_id, status,
  task_id) under pg_advisory_xact_lock + FOR UPDATE, rejecting status events
  whose task no longer owns the session. All orchestrator/runner/gRPC status
  transitions route through it and carry task_id end-to-end.
- Propagate expected_epoch to every terminal task write (error/status/output/
  usage/retry); CAS conflicts now abort the write instead of overwriting a
  reowned task.
- failover_or_fail_task returns Optional[int]; callers check "is not None" so
  the first failover (retry_count == 0) is actually requeued.
- Startup recovery respects max_retries for stale scheduling tasks; sandbox
  cleanup/orphan rescue go through failover; cancel() is a single CAS UPDATE;
  runner idle-before-result and non-terminal result are handled defensively.

Co-Authored-By: Claude-Opus-4.8[1m] <noreply@anthropic.com>
…istener

The command listener now runs each dispatched command through _dispatch_inner
and RPUSHes an ack (ok/error, keyed by command_id, TTL 30s) to the caller's
ack_key, refusing ack_keys without the expected prefix. This lets the API side
confirm a control input/cancel/shutdown was actually delivered to the local
bridge instead of assuming success on publish. The API-side requester lands in
the session-events commit.

Co-Authored-By: Claude-Opus-4.8[1m] <noreply@anthropic.com>
…TL delivery

Harden the task/session write endpoints:
- user.message is idempotent via Idempotency-Key: a partial unique index on
  (session_id, payload->>'_idempotency_key') plus IntegrityError replay makes
  a retried submit return the original event/task instead of double-dispatching.
- Per-user concurrent-task admission (structured 429) alongside the existing
  per-project gate; task rows now carry the submitter's user_id/org_id.
- Task/session dispatch marks the session running/idle through the task-scoped
  primitive and compensates to idle on enqueue failure; create-task and
  send-event reject archived/terminated/rescheduling/running/active-task
  sessions.
- HITL control inputs wait for the command listener's ack before returning,
  falling back to a retry task only on real non-delivery; unified
  enqueue_joysafeter_task() works in both monolith and split-service modes.

Co-Authored-By: Claude-Opus-4.8[1m] <noreply@anthropic.com>
Generate a per-submit UUID (crypto.randomUUID with a timestamp fallback) and
pass it as the Idempotency-Key header so a retried "send message" returns the
original task/event instead of dispatching a duplicate agent run.

Co-Authored-By: Claude-Opus-4.8[1m] <noreply@anthropic.com>
…ce keys

The per-user concurrent-task quota keyed on auth_ctx.user_id, which for API-key
auth is the key's creator — so every task from one service key shared a single
human's budget and could be silently clamped (default 5) below the project
limit. Add principal_type to the auth context ("api_key" for key auth) and
apply the per-user fairness gate only to human principals; service keys remain
bounded by the per-project quota. Attribution (task.user_id/org_id) is
unchanged.

Co-Authored-By: Claude-Opus-4.8[1m] <noreply@anthropic.com>
The provisioning-timeout path reset scheduling tasks to pending unconditionally
(reset_sandbox_tasks_to_pending), ignoring max_retries — a task whose sandbox
kept failing to provision could be requeued forever. Route it through
TaskController.failover_or_fail_task like sandbox-death recovery, so exhausted
tasks fail instead of looping, and only retryable ones are requeued. Removes the
now-dead reset_sandbox_tasks_to_pending / reset_sandbox_scheduling_to_pending.

Co-Authored-By: Claude-Opus-4.8[1m] <noreply@anthropic.com>
The archive endpoint archived sessions and the agent in two separate service
calls, each committing on its own — a failure of the second left sessions
archived but the agent live (or the reverse). Replace with a single
archive_agent_with_sessions() that validates active tasks once and commits both
writes together, so archival is all-or-nothing.

Co-Authored-By: Claude-Opus-4.8[1m] <noreply@anthropic.com>
…sage

The result handler, reconnect-result handler, and failover path each inlined
their own "if no agent.message exists, synthesize one from the task output"
logic. Consolidate into SessionService.repair_missing_agent_message(), which
no-ops on empty output or when the task already has an agent.message (a task
legitimately emits many), so the recovery paths can't drift and the emit stays
idempotent. Removes the duplicated gRPC _task_has_agent_output helper.

Co-Authored-By: Claude-Opus-4.8[1m] <noreply@anthropic.com>
The resumed-task and main streaming loops each carried an identical ~55-line
block to persist and fan out a session.status_running/idle event (event-bus
path vs. direct DB write + task WS broadcast + cross-instance Redis + SSE).
Collapse both into AgentBridgeServicer._emit_task_scoped_status_event so the
fan-out logic has a single definition and cannot drift between the two loops.
Behavior is unchanged; each call site passes its own session/task/coordinator.

Co-Authored-By: Claude-Opus-4.8[1m] <noreply@anthropic.com>
Both call sites of _emit_task_scoped_status_event resolved coordinator to
get_redis_coordinator(); fetch it inside the helper so the two call sites are
identical and the parameter list is smaller.

Co-Authored-By: Claude-Opus-4.8[1m] <noreply@anthropic.com>
yuzzjj and others added 4 commits July 6, 2026 09:24
… types

bus.py, event_persist.py, and stream_publisher.py each defined their own
identical _STATUS_EVENT_TYPES set to decide which events the two-phase bus
routes persist-then-broadcast (and the async persisters skip). Hoist the one
canonical set to envelope.py as STATUS_EVENT_TYPES so the routing layer cannot
silently drift out of sync. No behavior change (all three sets were identical).

Co-Authored-By: Claude-Opus-4.8[1m] <noreply@anthropic.com>
SessionService (4 sites) and the worker batch writer both derived the
pg_advisory_xact_lock key inline as int.from_bytes(session_id.bytes[8:], ...).
These acquirers MUST derive the key identically to mutually exclude (a
documented lock-ordering contract), yet it was enforced by copy-paste. Extract
session_advisory_lock_key() in joysafeter_shared.utils.locks so the derivation
can't drift between the two lock acquirers. No behavior change.

Co-Authored-By: Claude-Opus-4.8[1m] <noreply@anthropic.com>
…estricted-syntax

Co-Authored-By: Claude-Opus-4.8[1m] <noreply@anthropic.com>
Map FastAPI HTTPException to typed AppError (auth/access/notfound/conflict/
validation/ratelimit/unavailable) and inject the OTel trace_id into error
payloads. On the frontend, surface trace_id (payload or x-trace-id header),
truncate non-JSON error bodies, and fall back to the raw server message.
Adds api-client and managed/errors unit tests.

Co-Authored-By: Claude-Opus-4.8[1m] <noreply@anthropic.com>
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.

2 participants