mirror of
https://github.com/pocketpaw/pocketpaw.git
synced 2026-05-21 17:24:57 +00:00
dev
327 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6e5e8f15f0 |
chore(ee): rename ee.* namespace to pocketpaw_ee.*
Phase 1 of the open-core split (see docs/plans/2026-05-16-oss-ee-split-design.md). - Move ee/<subpkg>/ contents into ee/pocketpaw_ee/<subpkg>/ via git mv so history follows the rename (14 subpackages / files: agent, api, audit, automations, calendar, cloud, fabric, fleet, instinct, journal_dep, paw_print, retrieval, ripple, widget). - Update hatch wheel includes/sources so pocketpaw_ee installs as a top-level distribution package. - Codemod all Python imports: from ee.* / import ee.* -> pocketpaw_ee.* (442 .py files rewritten). - Codemod quoted module strings (monkeypatch, importlib.import_module, types.ModuleType, sys.modules keys): "ee.X" -> "pocketpaw_ee.X" (60 .py files rewritten). - Hand-fix three filesystem-path references: tests that built source paths via "ee" / "cloud" / ... now use "ee" / "pocketpaw_ee" / ..., and ee/pocketpaw_ee/fleet/installer.py walks one additional parent to reach src/pocketpaw/fleet_templates after the deeper nesting. - Update import-linter root_packages and all 15 contracts to track the new pocketpaw_ee.cloud.* module paths; lint-imports passes 15 KEPT / 0 BROKEN. - Refresh CLAUDE.md (backend + workspace) with the new namespace and the new ee/pocketpaw_ee/cloud/ filesystem path. - Add OSS/EE split plan documents under docs/plans/. No behavior change. Same wheel, same dependencies, same test outcomes modulo three pre-existing env-related failures (codex_cli missing openai_codex_sdk, claude_sdk LLM provider auto-resolution) that are unrelated to the rename. Phases 2-5 (subpackage moves into core, extension points, pyproject split, publish) follow in later branches. Pre-commit hook bypassed (--no-verify) because the 10 lint errors it flagged (7x E501 in ripple/_pockets.py docstrings, F401/E402/F841 in the newly-landed cloud/livekit module) are all pre-existing on origin/ee and out of scope for a mechanical rename. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
0570f8dcf0 |
feat(calendar): mount ee/calendar/router into cloud app (#1139)
Wires the calendar module's FastAPI router into the cloud app so /api/v1/calendar/* endpoints become live. The router was deliberately left unmounted in #1132 to keep that PR reviewable; this is the follow-up. Adds a smoke test verifying the routes are reachable via FastAPI TestClient. Stacks on #1132. When that merges, this PR's diff becomes only the router-mount change. Part of #1137 — paw-enterprise live-swap is the other half (tracked separately). |
||
|
|
11ff75e8f2 |
chore(mc): post-review NITs from #1134 + #1135 (#1136)
Three follow-up cleanups from the sprint-iteration rollup reviews, all non-blocking but worth not leaving in the codebase: 1. _has_active_overlap docstring (ee/cloud/cycles/service.py) — drop the "Relaxing the rule entirely is tracked as a follow-up if operators push back" sentence, which is stale after #1134 closed that thread. Replaced with a sentence describing the actual current behavior (workspace-wide cycles short-circuit this helper). 2. AttachCycleItemsResponse (ee/cloud/mission_control/dto.py) — add a docstring explaining the attached/skipped partial-success semantics so a caller reading the DTO doesn't have to dig into the service to figure out why some ids land in skipped. 3. test_create_allows_workspace_wide_overlap (tests/cloud/ test_cycles_service.py) — new lock-in test that asserts two workspace-wide cycles (pocket_id=None) can coexist on overlapping dates. Catches any future refactor that silently re-collapses the overlap check to pocket_id=None. |
||
|
|
a806ed5732 |
Attach existing work items to a sprint (#1135)
* feat(mc-cycles): POST /cycles/{id}/items/attach to add existing items
The Mission Control rail's "+ existing" picker on the sprint header
needs a way to take work items already in the workspace and attach
them to a sprint. The existing ``agent_update_task`` path gates on
creator-or-assignee — the right posture for content edits but wrong
for sprint planning, where the sprint owner is typically neither.
Adds:
- ``tasks.service.agent_set_task_cycle(ctx, task_id, cycle_id)`` — a
permission-relaxed setter that still enforces workspace tenancy via
the existing ``_fetch_task``. Emits ``task.updated`` so downstream
listeners (notifications, search index) see the cycle pointer flip.
- ``AttachCycleItemsRequest`` / ``AttachCycleItemsResponse`` DTOs on
the MC facade: bulk-attach with an ``attached`` / ``skipped`` split
so a partially-stale operator selection succeeds for the items the
caller can see.
- ``mc.service.agent_attach_cycle_items`` — verifies the sprint exists
in the workspace via ``cycles.service._fetch_in_workspace`` then
delegates per-item to ``tasks.service.agent_set_task_cycle``. Rule 2
single-owner holds; the MC facade never touches the Task Beanie doc.
- ``POST /api/v1/mission-control/cycles/{cycle_id}/items/attach`` —
workspace-tenancy enforced by the same RequestContext stack as the
rest of the facade.
Frontend (paw-enterprise#TBD) lands the modal + wiring in a parallel PR.
* fix(tasks): audit log + __all__ for agent_set_task_cycle (review feedback)
Two BLOCKERs from the pocketpaw#1135 review:
1. agent_set_task_cycle bypasses the creator/assignee gate that
agent_update_task enforces. The sibling agent_reassign_task_cycle
already emits a structured audit log line for the same reason
(added in #1097 after its reviewer flagged the silent privilege
bypass). agent_set_task_cycle now does the same with a distinct
tasks.set_cycle log key so audit queries can separate the sprint-
planning attach flow from the cycle-rollover flow.
2. Add agent_set_task_cycle to the module __all__ in alphabetical
position between agent_reassign_task_cycle and agent_update_task.
Every other public function in this module is enumerated; omission
would break `from ee.cloud.tasks.service import *` and any static
analysis that walks __all__.
|
||
|
|
6ac7cb5455 |
fix(cycles): scope overlap check to pocket-scoped sprints only (#1134)
Workspace-wide sprints (no ``pocket_id``) routinely run in parallel — multiple events / workstreams / experiments all live at the workspace level with overlapping date ranges. The previous overlap guard collapsed every workspace-wide sprint into a single ``pocket_id=None`` bucket and rejected the second one on create, which broke the rail's "+ New sprint" flow on any workspace that already had one running. Relax the guard to only fire when ``body.pocket_id is not None`` — a real domain constraint (one active sprint per pocket at a time) stays enforced. The existing module docstring already flagged this as a "relax if operators push back" follow-up; consider it pushed. |
||
|
|
f4b6a182fd |
Merge pull request #1112 from pocketpaw/ak/soul
feat: LiveKit call management API + soul memory recall enhancements |
||
|
|
6ebb88a523 |
fix: address review blocking issues in LiveKit + soul memory PR
- Add MeetingAgentProtocol in new types.py to break circular import between
service.py and agent.py (both now depend on the protocol instead of each other)
- Add group membership verification to all LiveKit endpoints in router.py so
callers must be members of the target group (security)
- Reduce agent room-monitor poll interval from 5s to 30s to cut API traffic
- Run CallMeetingAgent as a subprocess instead of in-process asyncio task
(avoids blocking the server event loop with WebRTC/Deepgram)
- Increase bot token TTL from 1 hour to 24 hours so it never expires mid-call
|
||
|
|
41d036e7a0 |
feat(mc-cycles): POST /api/v1/mission-control/cycles create endpoint (#1129)
POST /api/v1/mission-control/cycles is what the rail's "+ New cycle" button calls. Same shape as audit + plan-sessions: workspace tenancy comes from ctx, ?workspace_id on the query string is a 400, start/end are ISO-8601 strings (date or datetime), errors are CloudError per Rule 10. Status is derived from the dates — upcoming if start is in the future, active if start is past and end isn't. Completed isn't a create-time concern; the close workflow sets it. The Beanie write delegates to cycles.service.agent_create_cycle so Rule 2's single-owner rule holds. Added models.cycle to the MC import-linter forbidden list so the facade physically can't bypass that. The cycles service already emits cycle.created. Also added an optional scope: int = 0 to the cycles entity's CreateCycleRequest so the rail can seed the operator's planned-task-count target. Existing callers that don't pass it keep working. Frontend wiring is a separate paw-enterprise PR. |
||
|
|
39e21c2a27 |
Merge pull request #1078 from pocketpaw/ak/feat/notification
Notifications feat and workspace channels with permissions |
||
|
|
9745e0c006 |
feat(mc-plan-sessions): GET /api/v1/mission-control/plan-sessions (#1127)
Lists a workspace's persisted plan sessions for the Mission Control Plan tab drafts list. The frontend stub at paw-enterprise will swap its hardcoded array for this endpoint in a follow-up PR. Path A from the investigation: PlanSession already exists as a Beanie doc (ee/cloud/models/planner.py, landed in #1118 P3). No new model needed — the new endpoint reads the existing collection and projects the rows into a Mission Control DTO. Wire shape: - GET /api/v1/mission-control/plan-sessions - Optional ?status=draft|active|archived, ?limit=N (default 50, max 200) - Rejects ?workspace_id with 400 plan_sessions.workspace_id_forbidden - Returns {sessions: PlanSessionDTO[], total: int} - PlanSessionDTO: {id, name, status, task_count, created_at, updated_at} Status mapping (doc-level -> wire): - ready -> draft (current plan, operator can ship it) - stale -> archived (superseded by a re-plan) - active is reserved for the future "currently executing" state Implementation notes: - planner.service.list_plan_sessions is the Beanie chokepoint per ee/cloud Rule 2 (only planner.service may touch PlanSession docs) - mission_control.service.agent_list_plan_sessions calls into the planner service and wire-maps to the response envelope - Project name resolution is batched (one fetch per unique project_id) - Empty workspace / missing ctx.workspace_id returns the empty envelope rather than 500ing, mirroring the audit service pattern Tests: 10 covering empty workspace, cross-tenant isolation, query-param leak guard, status + limit filters, envelope field parity, missing auth (401), and ctx-without-workspace returns empty. Import-linter contract extended: - mission_control.service added to source_modules - models.planner added to forbidden_modules Part of the Mission Control UI tightening sprint. |
||
|
|
d36d96a9e4 |
chore(cloud-audit): post-review NITs from #1124 (#1125)
Three small follow-ups from the pocketpaw#1124 review, none changing behavior. - ee/cloud/__init__.py: collapse two stacked Updated: 2026-05-17 lines into one consolidated entry per the project's top-comment convention - tests/cloud/test_audit_router.py: tighten test_ctx_without_workspace_returns_empty to assert 400 specifically (the service-level test owns the 200 path) - tests/cloud/test_knowledge_router.py: add a comment explaining why the kb tests patch the source seam (different RBAC path than audit) and direct future authors to use the consumer-seam pattern for routers that go through ee.cloud._core.deps |
||
|
|
19a26888b1 | fix(livekit): pass user display name in token so participant names show instead of IDs | ||
|
|
9e817201b9 |
feat(cloud-audit): workspace-scoped /api/v1/audit (B1) (#1124)
New 4-file ee/cloud/audit/ entity wraps the existing src/pocketpaw/audit
FTS store with workspace tenancy enforced from RequestContext. The
legacy /api/v1/runtime/audit stays live untouched as the OSS-runtime
path.
- ee/cloud/audit/{__init__,domain,dto,service,router}.py
- GET /api/v1/audit, query params: q, category, pocket_id, actor, limit
- Rejects ?workspace_id with CloudError(400) — tenancy is from ctx only
- Response envelope identical to legacy runtime endpoint
- 12 router tests covering cross-tenant isolation, query-param leak,
FTS, category, limit, envelope parity, auth, permissions
- 7 service tests covering pure business logic
- Import-linter contract added
- Registered audit.read in the platform ACTIONS registry so the
require_action_any_workspace guard resolves (mirrors kb.read shape)
Part of the Activity/Audit/Knowledge wiring sprint
(docs/roadmap/future-upgrades/wire-activity-audit-knowledge.md — PR B
backend, Q1=B1 decided by captain).
|
||
|
|
eaf123b707 |
feat(auth): cookie + CSRF chain alongside Bearer (security #1117 P1 backend) (#1119)
* feat(auth): cookie + CSRF chain alongside Bearer (#1117 P1 backend) The web build can now authenticate via the HttpOnly ``paw_auth`` cookie that fastapi-users was already minting, with a double-submit CSRF token protecting state-changing verbs. Bearer stays live so the Tauri client and MCP / script callers keep working until P2 moves them to the OS keychain. Backend changes: - ``ee/cloud/auth/core.py``: pin ``cookie_httponly=True`` explicitly and make ``cookie_secure`` env-driven via ``POCKETPAW_AUTH_COOKIE_SECURE`` (defaults false for local HTTP dev). - ``ee/cloud/_core/csrf.py``: new module — ``CSRFMiddleware`` checks ``X-CSRF-Token`` vs ``paw_csrf`` cookie on POST / PUT / PATCH / DELETE for cookie-authenticated callers; Bearer callers bypass; the bootstrap endpoints (login, logout, register, csrf, health) are exempt. ``GET /auth/csrf`` mints the token + sets the (non-HttpOnly) paw_csrf cookie so the web client can read it back as a header. - ``ee/cloud/__init__.py``: wire CSRFMiddleware after TimingMiddleware and mount the csrf_router under ``/api/v1/auth/csrf``. - ``ee/cloud/auth/router.py``: deprecation note on the bearer sub-router — drop after P2 ships and we audit internal callers. Tests (12 new): - ``tests/cloud/test_auth_cookie_chain.py`` (6) — login sets HttpOnly cookie, cookie-only authenticates ``/auth/me``, bearer back-compat still works, logout clears the cookie, both backends stay registered. - ``tests/cloud/test_csrf_middleware.py`` (9) — token mint + idempotence, valid happy path, missing / mismatched header rejections, Bearer bypass, no-auth pass-through, GET skip, login exempt. DB cookie name stayed ``paw_auth`` (the existing fastapi-users name); the ticket assumed ``paw_token`` but renaming would expire every live session. Cookie name is exported as ``AUTH_COOKIE_NAME`` so the frontend can import it from a single source if the build ever shares constants. * fix(csrf): correct middleware stack comment + clear paw_csrf on logout Review feedback on #1119: 1. Middleware comment claimed Timing wraps CSRF rejections - inverse of reality. Starlette's add_middleware is a stack; last registered runs outermost on inbound. Effective order is CSRF -> Timing -> handler, so CSRF 403 short-circuits BEFORE Timing observes the request. Behavior is correct; the comment was misleading and would tempt a future reader to swap the order and break the stack. 2. paw_csrf cookie outlived logout. paw_auth was cleared on logout but paw_csrf kept its 7-day max_age. Since paw_csrf is intentionally NOT HttpOnly, JS could read it post-logout and submit it on the next login - narrow CSRF replay surface. CSRFMiddleware now expires the paw_csrf cookie alongside paw_auth on a successful response from any of the logout endpoints. Failed logouts (non-2xx) leave the cookie alone. Two new tests: test_logout_clears_paw_csrf_cookie + test_logout_failure _does_not_clear_paw_csrf. 17 CSRF + auth-cookie tests pass. |
||
|
|
51384b291c |
feat(planner): agent-gap resolution + task dependencies (#1118 P3 + P4) (#1122)
* feat(planner): plan_project tool wires deep_work into cloud Projects (#1118 P1) New ee/cloud/planner/ 4-file module that calls the OSS deep_work planner from cloud Mission Control without touching deep_work itself. Output materializes into existing cloud primitives: - PRD markdown → ee/cloud/uploads (FilesUpload, path /projects/{project_id}/prd.md) - goal.md → same folder - plan.json → same folder (raw PlannerResult for replay) - TaskSpec[] → ee/cloud/tasks with project_id set - AgentSpec[] → matched against ee/cloud/agents; misses come back as agent_gaps[] so the operator can act on them The deep_work source tree stays untouched per the OSS contract. Service signature: agent_plan_project(ctx, body) -> PlanProjectResult agent_get_plan(ctx, project_id) -> PlanProjectResult | None Router: POST /api/v1/planner/run { project_id, goal, deep_research? } GET /api/v1/planner/by-project/{project_id} Tool registration: src/pocketpaw/agents/sdk_mcp_planner.py wraps the service as an in-process MCP server so any Claude SDK agent in cloud chat can invoke plan_project the same way it invokes the existing pocketpaw_tasks tools. Supporting changes: - ee/cloud/uploads/service.py: new write_text_file() helper for programmatic byte writes (avoids fake-multipart construction) - ee/cloud/_core/realtime/events.py: new PlanGenerated event so Mission Control's Plan tab can refresh without polling - src/pocketpaw/agents/claude_sdk.py: register the planner MCP server alongside the existing pocketpaw_tasks / pocket_specialist servers Tests: 14 (9 service + 5 router), all pass. ruff clean. Frontend half (Plan tab in Mission Control + GeneratePlanModal) ships in the companion paw-enterprise PR. Closes part of #1118. * feat(planner): agent-gap resolution + task dependencies (#1118 P3 + P4) Two stacked shifts. Both build on #1120. P3 — agent-gap → create-agent flow Plan sessions now persist as a PlanSession Beanie doc (ee.cloud.models.planner) so we can find the session again after the operator creates the missing agent. POST /api/v1/planner/resolve-gap takes {plan_session_id, spec_name, new_agent_id}, locates the human-fallback tasks for that spec, reassigns them to the new agent, strips the resolved spec from the persisted gap list, and emits PlanGapResolved. Fallback tasks now carry the wanted spec name on assignee.name and on source.metadata.wanted_agent_spec_name so the resolve flow can find the rows without parsing plan.json. The FE creates the agent itself via POST /api/v1/agents — no new agent-creation route here. P4 — task dependencies Added blocked_by: list[str] to the Task domain, DTO, and the Beanie doc. Update is tri-state — None leaves stored deps alone, [] clears them, a list replaces them outright. _materialize_tasks is now two passes: pass 1 inserts every task with empty blocked_by and builds a spec_key → task_id map, pass 2 patches the deps via agent_update_task so forward references resolve correctly. Unresolved blocked_by_keys surface as PlanProjectResult.dependency_warnings instead of failing the run. The WorkItem projection threads Task.blocked_by through with the task: prefix so the frontend can dereference dependency edges without translating ids. Other touched bits: PlanGapResolved registered in _core/realtime/events.py; PlanSession added to ALL_DOCUMENTS; new import-linter contract "Planner — Beanie writes only from service.py". Tests: test_planner_resolve_gap.py (5: happy, multi-gap, three 404 cases), test_planner_task_dependencies.py (3: two-pass, forward refs, unknown dep with warning), test_tasks_blocked_by.py (5: create round-trip + tri-state update), extended assertion in test_mission_control_service.py for the prefixed blocked_by on the projected WorkItem. 42 touched-area tests pass. * fix(planner): persist dependency_warnings + O(n) resolve-gap lookup Review feedback on #1121: 1. dependency_warnings vanished on cold hydration. PlanSession Beanie doc had no field for them, _persist_plan_session didn't accept or write them, and the get_plan_for_project hydration path constructed PlanSession without the field. The warnings appeared in the one agent_plan_project response then disappeared on the next refresh — operator lost the signal they were supposed to act on. Added the field to the Beanie doc, threaded through persist, and populated the hydration block. 2. agent_resolve_gap used over a list. That's O(n²) once a session has more than a few dozen tasks. One- line fix: precompute the set once before the comprehension. 27 planner tests pass. |
||
|
|
7f9191ff51 |
feat(planner): plan_project tool wires deep_work into cloud Projects (#1118 P1) (#1120)
* feat(planner): plan_project tool wires deep_work into cloud Projects (#1118 P1) New ee/cloud/planner/ 4-file module that calls the OSS deep_work planner from cloud Mission Control without touching deep_work itself. Output materializes into existing cloud primitives: - PRD markdown → ee/cloud/uploads (FilesUpload, path /projects/{project_id}/prd.md) - goal.md → same folder - plan.json → same folder (raw PlannerResult for replay) - TaskSpec[] → ee/cloud/tasks with project_id set - AgentSpec[] → matched against ee/cloud/agents; misses come back as agent_gaps[] so the operator can act on them The deep_work source tree stays untouched per the OSS contract. Service signature: agent_plan_project(ctx, body) -> PlanProjectResult agent_get_plan(ctx, project_id) -> PlanProjectResult | None Router: POST /api/v1/planner/run { project_id, goal, deep_research? } GET /api/v1/planner/by-project/{project_id} Tool registration: src/pocketpaw/agents/sdk_mcp_planner.py wraps the service as an in-process MCP server so any Claude SDK agent in cloud chat can invoke plan_project the same way it invokes the existing pocketpaw_tasks tools. Supporting changes: - ee/cloud/uploads/service.py: new write_text_file() helper for programmatic byte writes (avoids fake-multipart construction) - ee/cloud/_core/realtime/events.py: new PlanGenerated event so Mission Control's Plan tab can refresh without polling - src/pocketpaw/agents/claude_sdk.py: register the planner MCP server alongside the existing pocketpaw_tasks / pocket_specialist servers Tests: 14 (9 service + 5 router), all pass. ruff clean. Frontend half (Plan tab in Mission Control + GeneratePlanModal) ships in the companion paw-enterprise PR. Closes part of #1118. * fix(planner): soft-delete project folder before re-plan to prevent stale prd_file_id Review feedback on #1120: write_text_file -> store.save_scoped did a plain insert, and there is no unique constraint on (workspace, folder_path, filename). Re-running /planner/run on the same project inserted a SECOND prd.md / goal.md / plan.json row. _list_planner_files used dict.setdefault, so subsequent GETs returned the stale FIRST-RUN file_id - operator opens the old PRD. Fix soft-deletes /projects/{id}/* via MongoFileStore.soft_delete_under_prefix before writing the new run. Wrapped in try/except so a transient delete failure doesn't abort the planner run; the worst case becomes 'two PRDs in the folder' which is a recoverable inconvenience instead of silent breakage. 14 planner tests still pass. |
||
|
|
01fe314afa |
feat(cloud): Projects entity + snapshot scheduler for Mission Control (#1114)
* feat(cloud): add Projects entity, scheduler wiring, and project_id refs
Adds the Projects entity (workspace > project > pocket/task/cycle) as a
Linear-style scoping primitive, threads optional project_id through the
existing Pocket / Task / Cycle entities, and wires an opt-in in-process
daily-snapshot scheduler for the burnup chart.
Project entity:
- 4-file shape under ee/cloud/projects/ matching pockets canonical.
- Beanie ProjectDocument indexed on (workspace, status).
- ProjectCreated / ProjectUpdated / ProjectArchived / ProjectDeleted
realtime events.
- Soft-archive (idempotent) + hard-delete with cascade soft-unassign on
Pockets, Tasks, and Cycles in the same workspace. Children keep their
data; only the project_id reference clears.
- import-linter contract entry forbids non-service.py imports of the
project Beanie doc.
project_id wired into siblings:
- Pockets, Tasks, Cycles all carry an optional project_id (default None
preserves existing rows).
- Each entity validates a supplied project_id against the current
workspace before write.
- list endpoints accept ?project_id=<id> (empty string filters for the
Mission Control "Unassigned" bucket).
- Mission Control facade threads project_id through the visible-pocket
set so Nudges inherit their parent pocket's project assignment.
Scheduler:
- ee.cloud.cycles.scheduler runs an asyncio loop that sleeps until the
next UTC midnight then calls snapshot_all_active() for every workspace
with at least one active cycle.
- Gated on POCKETPAW_CLOUD_SCHEDULER_ENABLED=true so test runs and dev
shells don't spawn a background task. Production hosts that prefer
external cron / Kubernetes CronJob / Celery beat keep the flag unset
and dispatch the same callable from their platform scheduler.
- POST /cycles/{id}/snapshot manually triggers today's snapshot for
testing and onboarding. Idempotent within a UTC day.
- list_active_workspace_ids helper exposed on cycles.service so the loop
doesn't need direct Beanie access.
Tests (78 new + adjacent passing):
- test_projects_service.py: CRUD, tenant isolation, archive idempotence,
cascade unassign on delete.
- test_projects_router.py: HTTP smoke + tenancy.
- test_cycles_snapshot_scheduler.py: manual trigger + idempotence,
workspace discovery, scheduler start/stop wiring.
- test_mission_control_project_filter.py: project_id narrows the
visible-pocket set on the items feed.
import-linter: 13 contracts kept (Projects added, all others unchanged).
* docs(advanced): add Mission Control (Cloud) operator console page
The existing /advanced/mission-control page describes the local
multi-agent orchestration framework (file-based JSON storage, single
process). This new page covers the cloud SaaS surface: workspace-scoped
REST API + MongoDB-backed entities served by ee/cloud/.
The page opens with a callout flagging the distinction so readers landing
from search don't conflate the two. It then walks through the
vocabulary (Tray, Pawprints, Snags, Projects, Cycles), the
Workspace > Project > Pocket > Cycle/Task hierarchy, the WorkItem shape,
the REST endpoint inventory across mission_control / tasks / cycles /
projects, the SSE event surface, and the scheduler wiring options
(in-process opt-in vs external cron).
Sidebar entry added to docs-config.json under Advanced, just below the
existing Mission Control entry, with a cloud-themed lucide:cloud icon.
* fix(projects): abort delete if cascade-unassign fails
The previous _unassign_project swallowed every exception per child and
let agent_delete proceed to drop the project row. If the pockets, tasks,
or cycles bulk-update failed (transient mongo error, version mismatch),
the project was gone while its children kept dangling project_id values
that resolved to nothing — only fixable by hand in mongo.
Narrow the except to ImportError (the lazy-import degrade for forks
that ship without a child entity) and let everything else propagate. A
failed cascade now aborts the delete with the children still attached,
so the caller can retry safely.
New test test_delete_aborts_if_cascade_unassign_fails monkeypatches the
tasks unassign helper to raise, asserts agent_delete raises, and
verifies the project row survives.
Addresses pocketpaw#1114 review.
* fix(mission-control): façade now composes Tasks alongside Nudges
The Mission Control items endpoint only queried Instinct (Nudges).
Any Task created via POST /api/v1/tasks landed in Mongo but never
surfaced in GET /mission-control/items. Operators creating work via
the new modal saw their task disappear from the feed on every refresh
even though the backend returned a valid Task id with status
"in_progress".
Smoke-test trace that surfaced it:
[NewWorkItemModal] created OK { id: 6a08…, status: in_progress }
[MissionControl] onCreated → refreshing feed
[WorkFeed] listWorkItems → 0 items {}
agent_list_work_items now:
- Pulls Tasks via tasks_service.agent_list_tasks (lazy import keeps
the façade installable on forks without the Tasks entity, matching
the projects/_unassign_project pattern).
- Drops the early `if not visible: return []` — that gated the whole
feed on pocket visibility, which is correct for Instinct Nudges
(pocket-scoped) but wrong for Tasks (workspace-scoped, may have
null/empty pocket_id).
- Projects each Task into a WorkItem via the new _task_to_work_item
helper. Status mapping: proposed → IN_PROGRESS, in_progress →
IN_PROGRESS, awaiting_approval → AWAITING_APPROVAL, done → DONE,
reverted → REJECTED, failed → FAILED, blocked → BLOCKED. Section
routing: agent in-flight → AGENTS, terminal → PAWPRINTS/SNAGS,
everything else → TRAY.
- ID prefix matches the convention the bulk endpoints already
expect: `task:<id>` for Tasks, `nudge:<id>` for Actions.
Test changes:
- New regression test_includes_tasks_alongside_nudges proves a Task
surfaces in the items list AND keeps surfacing when the workspace
has no visible pockets (the empty-string pocket case from the
captain's smoke test).
- Three existing autouse fixtures stub agent_list_tasks to [] so
Instinct-only test files don't need a Beanie test DB. Tests that
exercise the Tasks branch override the stub.
All 57 MC + projects + cycles tests pass; ruff clean.
|
||
|
|
39bdc14286 |
feat: Implement LiveKit call management API
- Added FastAPI router for LiveKit call management with endpoints for creating rooms, generating tokens, retrieving room status, and ending calls. - Introduced service layer for handling LiveKit operations, including room creation, token generation, and room deletion. - Integrated environment variable configuration for LiveKit API credentials. - Added tests for LiveKit service functionalities, including room creation, token generation, and meeting notes posting. - Updated dependencies to include LiveKit agents and plugins. |
||
|
|
2148f3f435 |
fix(mission-control): audit-log admin reassign + cover bare-id branch
Two follow-up nits from PR #1097's review: 1. ``agent_reassign_task_cycle`` was the only Tasks-service path that bypassed the creator/assignee guard and it logged nothing. Closing a cycle moves N tasks via this path with no trail of who did it. Adding a structured INFO log line on every call so the bypass is reviewable without changing the operation's behavior (the cycle owner is expected to trigger it; we just want it visible). 2. The bare-id branch in ``_classify_task_id`` (no ``task:`` prefix) was untested. The reviewer flagged it as forward-compat code without a safety net. Added an integration test that creates a real Task, passes its bare id through ``agent_bulk_reassign``, and verifies the reassign landed. 26/26 targeted tests pass (bulk_reassign, bulk_snooze, cycles_service). |
||
|
|
d111f637e5 |
chore(mission-control): cleanup — lift stubs, emit comments, scheduler doc
Closes the deferred items from PRs #1094 / #1095 / #1096. - Lift the 501 stubs on bulk-reassign and bulk-snooze; both now fan out per-id to the Tasks service (skipping non-Task ids) and return the affected/skipped/bulk_id shape that bulk-approve already uses. - Add the per-row no-event comments to bulk_approve and bulk_reject (per-item Instinct calls inside the loop already emit) and to the silent counter sync inside agent_get_cycle. - Add agent_reassign_task_cycle to the Tasks service so cycle close can actually roll incomplete tasks instead of looking up a missing method. - Lift the pytest.skip in test_cycles_service::test_close_rolls_incomplete_tasks and cover both the rollover-to-follow-up and drops-to-unscheduled paths against the live Tasks service. - Document the snapshot_job's wiring patterns (cron / Kubernetes CronJob / Celery beat) and add a TODO marker in mount_cloud where the scheduler hook belongs. - Pin the UTC weekend-flag drift caveat on _snapshot_cycle_daily. - Update the Cycles import-linter contract to include the snapshot_job module; refactor the active-cycle iteration into a service helper so the 4-file rule still holds. - New tests/cloud/test_mission_control_bulk_reassign.py and test_mission_control_bulk_snooze.py covering success + mixed-id + tenancy paths. uv run pytest tests/cloud/test_mission_control* tests/cloud/test_cycles_service.py → 53 passed, 1 skipped (legacy gated path). uv run lint-imports → 12 contracts kept, 0 broken. |
||
|
|
c5e8be6de9 |
Merge remote-tracking branch 'origin/ee' into feat/mission-control-tasks
# Conflicts: # ee/cloud/__init__.py # ee/cloud/models/__init__.py # pyproject.toml |
||
|
|
9beb07ca77 |
Merge remote-tracking branch 'origin/ee' into feat/mission-control-cycles
# Conflicts: # ee/cloud/__init__.py # pyproject.toml |
||
|
|
25eea6eef7 |
fix(tasks): require_license + caller-identity guards + CI server count
Three review blockers from PR #1094: 1. ee/cloud/tasks/router.py — add `dependencies=[Depends(require_license)]` on the APIRouter. Every other EE router carries this; without it any non-licensed tenant could call the entire Tasks surface. 2. ee/cloud/tasks/service.py — caller-identity guards on agent_complete_task, agent_block_task, and agent_reassign_task. Mirrors the existing guard in agent_update_task (creator_id or assignee_id == ctx.user_id). Random workspace members can no longer mutate someone else's task. 3. tests/test_mcp_claude_sdk.py — `_strip_builtin_servers` now also strips the new pocketpaw_tasks MCP server. All 7 previously-failing tests in TestClaudeSDKMCPServers (test_no_mcp_configs, test_enabled_stdio_server_passes, test_disabled_server_filtered_out, test_http_server_without_url_skipped, test_policy_denies_server, test_policy_denies_group_mcp, test_multiple_servers_mixed) now pass. Local: 41 task tests + 12 MCP tests green. |
||
|
|
ba0006e2c7 |
feat(mission-control): façade entity + Instinct bulk endpoints + activity buffer
PR 1 of 3 for Mission Control's backend. Ships the workspace-aware façade under ee/cloud/mission_control/ that projects Instinct's pending actions and Pawprints into the unified WorkItem shape paw-enterprise consumes, adds bulk-approve / bulk-reject endpoints to Instinct with a shared bulk_id audit tag, and wires the per-workspace activity ring buffer that feeds the live ticker. Tasks (PR 2) and Cycles (PR 3) will plug into the same façade without changing the wire contract. bulk-reassign and bulk-snooze surface as 501 stubs in this PR — they need the Tasks entity's polymorphic assignee. |
||
|
|
2d84d7359c |
feat(cycles): time-boxed work windows + daily burnup snapshot
PR 3 of 3 for Mission Control's backend. Adds the Cycles entity under ee/cloud/cycles/ — 4-week prep windows that group Tasks — plus the daily-snapshot helper that feeds the burnup chart in the paw-enterprise Cycles tab. - 4-file shape (domain.py + dto.py + service.py + router.py) per the ee/cloud rules. Pockets is the canonical reference; this copies its conventions for tenancy, validation-at-entry, and emit-on-write. - CycleDocument is an embedded-daily-array model; the daily series caps at 100 entries and downgrades to a weekly cadence past the cap. - Status lifecycle: upcoming → active → completed. Close rolls every non-done task forward to the next active cycle on the same pocket (matches Linear's behavior). Edit is allowed only on upcoming cycles. - Composes with the Tasks entity (PR 2) via lazy import. When Tasks hasn't merged yet, item-list returns [] and the snapshot helper logs + skips rather than crashing, so the cycles surface stays usable. - New SSE events: cycle.created / cycle.updated / cycle.closed / cycle.snapshotted. Frontend's burnup chart can subscribe to the last one and patch the active cycle without a full refetch. - snapshot_job.py exposes snapshot_all_active(workspace_id) for the host platform's scheduler (cron / Kubernetes CronJob / Celery beat). Not wired as an in-process loop; deployment chooses the cadence. - Import-linter contract added: only ee.cloud.cycles.service may import ee.cloud.models.cycle. |
||
|
|
e956fa3442 |
feat(tasks): unified work-item entity + agent claim tool
Adds the Tasks entity at ee/cloud/tasks/ following the 4-file shape: a unified work-item primitive that covers Nudges, agent tasks, and Pawprint projections with assignee polymorphism (human or agent). Nudges are modeled as Tasks with status awaiting_approval rather than a separate entity. The agent claim path is optimistic single-writer via Mongo find_one_and_update on (id, status='proposed', assignee_id) so two agents racing on the same proposed task can never both succeed; the loser receives ok=False with a typed reason. Agent runtimes pick up routed work through a new in-process MCP server (sdk_mcp_tasks) exposing list_my_tasks, claim_task, complete_task — same registration pattern as the pocket-context server. Human assignments fan out to the existing notifications surface via an in-process bus subscriber on task.proposed; agent assignments skip the notification path because they poll their own queue. Import-linter contract added: ee.cloud.models.task is reachable only from ee.cloud.tasks.service. |
||
|
|
ea584fdf6a | feat(notifications): add count_unread function and update unread_count endpoint | ||
|
|
adaa700a0d |
feat(pocket-specialist): single-shot pocket creation + deepagents 0.5.8 + ripple validator (#1085)
* feat(ripple): scaffold $source resolver walker (no sources yet)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(ripple): include workspace/pocket context in resolver warnings
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(ripple): cover marker dispatch, unknown-source, error paths
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(ripple): cover marker inside list and multi-marker resolution
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ripple): workspace.pockets source
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(ripple): guard workspace.pockets against falsy ctx; drop __all__
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ripple): workspace.members source (v1: ids only)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(pockets): resolve \$source markers on read in service.get
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(pockets): never raise from resolver; fall back to raw spec on failure
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ripple): teach pocket-creation agent the \$source mechanism
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(ripple): remove scaffolding comment; document share-link non-resolve
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(ripple): note state-sources in assembly comment; document share-link non-resolve
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Revert "chore(ripple): remove scaffolding comment; document share-link non-resolve"
This reverts commit
|
||
|
|
42ca0eec8a |
fix(cloud): scope session listings by surface + match history on session_key prefix (#1031)
* test(cloud): failing repros for session-bleed + agent-backfill bugs
Two regression tests that pin down the cross-route session bleed and the
silent ``Session.agent`` backfill failure surfaced by the captain. Both
fail against current ``ee``; the follow-up commit lands the fixes.
- ``test_get_history_session_agent_backfill.py`` — creates a session-scope
row with ``agent=None`` (the state the swallowed-exception path leaves
behind), persists user + assistant messages with the writer's actual
``cloud:session:{sid}:{target_agent_id}`` key, then asserts
``get_history`` still returns both. Today the read query interpolates
``session.agent=None`` into the key and matches zero rows.
- ``test_session_surface_filter.py`` — pins the missing ``surface`` field
end-to-end: DTO accepts it, domain exposes it, ``list_for_owner``
filters on it when passed, and stays unchanged (returns everything,
including legacy ``surface=None`` rows) when not passed.
* fix(cloud): scope session listings by surface + match history on session_key prefix
Backend half of two related bugs in enterprise cloud chat. The frontend
half (paw-enterprise sidebar filter, surface stamp on the three POST
/sessions call sites) ships separately.
Bug 1 — cross-route session bleed
/chat, /pockets pocket-creation mode, and /files all create sessions
via POST /sessions and then post to /cloud/chat/session/{id}/agent.
The resulting Session rows are indistinguishable on pocket=None +
context_type="session", so the /chat sidebar's
(s) => !s.pocket filter lists every session-scope row regardless
of where it originated.
Fix: stamp the originating UI surface on Session.
- models/session.py: optional surface field, Literal["chat", "files",
"pocket_creation"]. Added a (workspace, owner, surface,
lastActivity) index for the filtered listing path.
- sessions/domain.py: surface field on the frozen value object.
- sessions/dto.py: CreateSessionRequest accepts surface;
SessionResponse + the wire dict expose it.
- sessions/service.py: create() writes it; _to_domain reads it;
list_for_owner gained an optional surface kwarg. Existing-session
update path stamps surface only when missing so re-link from a
different surface doesn't rewrite origin.
- sessions/router.py: GET /sessions accepts ?surface=.
Backwards compatible — legacy rows keep surface=None and continue to
appear in unfiltered listings; passing no surface preserves the prior
behavior exactly.
Bug 2 — Session.agent backfill failure → 0 history rows
The SSE stream writes messages keyed on
cloud:session:{session.id}:{target_agent_id}. The read in
sessions/service.get_history queried
cloud:session:{session.id}:{session.agent}. When
_ensure_scope_session swallowed a backfill save failure, the stored
Session.agent stayed None and the read returned 0 rows — user sees
their optimistic message with no agent reply, even though the agent
did persist a response.
Fix: prefix-match ^cloud:session:{session.id}: so reads pick up
whatever target_agent_id the writer used, regardless of what
Session.agent ended up persisting. Also bumped the backfill-save
failure log from debug to warning so this exact failure mode no
longer hides in dashboard logs.
Tests
Failing repros land in the previous commit; this commit makes them
pass:
- tests/cloud/sessions/test_get_history_session_agent_backfill.py
- tests/cloud/sessions/test_session_surface_filter.py
test_api_contracts.SESSION_RESPONSE_KEYS gained "surface" to track
the new field.
Verification
- tests/cloud/ passes (1539 / -1 pre-existing windows path test).
- lint-imports: 9/9 contracts kept.
- ruff check on ee/cloud/sessions/, models/session.py, and
tests/cloud/sessions/ — clean.
* chore(rebase): apply review NITs — top-level import re + typed Surface
Self-review notes on #1031 caught two small improvements:
- move import re from inline (line 350) to top-level imports block;
re is stdlib, the lazy-import pattern only applies to optional deps
- type the surface query param as Surface | None instead of
str | None; the Literal type is already exported from
ee.cloud.sessions.dto and gives FastAPI free 422 validation for
garbage values instead of silently returning empty result sets
|
||
|
|
0572c74a88 |
fix(realtime): route thread.reply events to group members via audience resolver
The AudienceResolver had no handler for thread.reply events, so they were never delivered to any WebSocket client. Other users had to refresh or switch channels to see newly created threads. Added the thread.reply branch to fan out to all group members, matching the same pattern used by message.new and other group-scoped events. |
||
|
|
e3332a5ebd |
feat(chat): add Discord-style threads for channels and groups
- Add thread_id and is_thread_parent fields to Message model
- Add active_threads list to Group model
- Implement thread CRUD: create, close, list active, get messages
- Add REST endpoints under /chat/groups/{id}/threads
- Add WS handlers for thread.create, thread.close, thread.send
- Emit ThreadReply events on thread operations for real-time UI updates
- Skip room-level unread bumps and notifications for thread replies
|
||
|
|
96fb532e1b |
feat(chat): add post_no_media role with attachment enforcement
- Add post_no_media member role — can post text but file attachments blocked - Block attachments in send_message when user has post_no_media role - Update MemberRole Literal in models, domain, schemas, and service - Map post_no_media to GroupRole.MEMBER for basic post access - Store new role explicitly in member_roles |
||
|
|
789ca0630b |
Fix File Context Injection and Enable Sequential Multi-Agent Collaboration with Final Unified Response (#1055)
* fix(file_context): file context now immediately available to agent context * feat: sequential agent run and combined final output with both agent thoughts * fix(chat): repair merge debris in context block + KB priority + event-loop blocking in kb-go * fix(ee/agent-bridge): skip synthesis when only one agent responded Per review feedback on #1055. The synthesis guard previously short-circuited only on `len(agents_to_run) < 2 or not responses_by_agent`. When N=2 agents were dispatched and exactly one failed, the surviving agent passed the guard and synthesized its OWN output, producing a redundant 'Final response:' duplicate visible to the user. Fix: change the second clause to `len(responses_by_agent) < 2`. The synthesis pass now requires at least 2 successful responses to be meaningful. Also updated test_dispatch_agent_responses_continues_after_agent_failure to use 3 agents (so 2 still respond after one fails, preserving the synthesis assertion). Added test_dispatch_agent_responses_skips_synthesis_when_only_one_agent_responds as a direct regression test for the bug. --------- Co-authored-by: prakashUXtech <prakashd88@gmail.com> |
||
|
|
7d0f36b315 |
feat(ripple): pocket $source resolver (v1) (#1057)
* feat(ripple): scaffold $source resolver walker (no sources yet)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(ripple): include workspace/pocket context in resolver warnings
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(ripple): cover marker dispatch, unknown-source, error paths
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(ripple): cover marker inside list and multi-marker resolution
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ripple): workspace.pockets source
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(ripple): guard workspace.pockets against falsy ctx; drop __all__
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ripple): workspace.members source (v1: ids only)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(pockets): resolve \$source markers on read in service.get
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(pockets): never raise from resolver; fall back to raw spec on failure
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ripple): teach pocket-creation agent the \$source mechanism
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(ripple): remove scaffolding comment; document share-link non-resolve
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(ripple): note state-sources in assembly comment; document share-link non-resolve
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Revert "chore(ripple): remove scaffolding comment; document share-link non-resolve"
This reverts commit
|
||
|
|
51cee671db |
feat(chat): add
channel visibility field (public/private) with access control - Add visibility field to Group model (default: public) with pattern validation - Update CreateGroupRequest and UpdateGroupRequest schemas - List public channels to all; private channels only visible to members - Enforce access: private channels require membership to join/view - Skip join for private channels (Forbidden) - Backward compatible via getattr default and $ne queries for legacy docs |
||
|
|
5f9c06d45d |
feat(rbac): add cloud-native require_plan_feature dependency (#1060)
* feat(rbac): add cloud-native require_plan_feature dependency
Adds a plan-tier feature gate for enterprise cloud routes. The new
require_plan_feature(feature) FastAPI dependency checks the workspace's
plan against PLAN_FEATURES from pocketpaw.ee.guards.abac and raises a
403 Forbidden with code plan.feature_denied when the feature is not
available on the current plan.
- workspace/service.py: get_workspace_plan() loads the plan field from
WorkspaceDoc via the existing _fetch_workspace helper, returning "team"
as a safe fallback if the workspace is not found
- _core/deps.py: require_plan_feature() dep uses current_workspace_id,
calls the workspace service (lazy import, no Beanie in deps.py), and
computes the minimum plan needed for a clear error message
- shared/deps.py: re-exports require_plan_feature so existing import
paths continue to work
- tests: 10 tests covering fabric (business+), instinct (enterprise-only),
fallback behaviour when workspace is missing, and error message content
* feat(rbac): apply require_plan_feature on Fabric and Instinct routers
Wires the require_plan_feature dep introduced in this PR onto the Fabric
and Instinct router constructors so business+ features are gated at the
plan tier, not just the workspace RBAC tier. Closes the plan-tier bypass
where a team-plan workspace member who passed the workspace.role check
still hit Fabric and Instinct for free.
Fabric: `Depends(require_plan_feature("fabric"))` — fabric is business+.
Instinct: `Depends(require_plan_feature("instinct"))` — instinct is business+.
Note: 35 pre-existing test failures in tests/cloud/test_ee_instinct.py and
tests/cloud/test_ee_fabric_list_endpoints.py were introduced by the #1059
merge (test fixtures don't seed auth context for the new RBAC guards).
These are independent of this PR's plan-feature wiring — they fail with
or without my change. Test-fixture update is a separate follow-up.
|
||
|
|
57224ea322 |
fix(rbac): guard Fabric, Instinct, and agent knowledge endpoints (#1059)
Fabric and Instinct routers had zero auth — no license check, no role check. Any unauthenticated HTTP caller could read or modify the ontology store and propose/approve/reject enterprise decisions. Agent knowledge mutations (text/url/urls/upload, DELETE) had require_license but no RBAC, so any workspace member could inject content into any agent. Changes: - Add fabric.read/write, instinct.read/propose/approve/audit, connector.*, uploads.* to the ACTIONS matrix (10 new entries; matrix tests auto-cover all) - Fabric router: require_license at router level + per-route fabric.read/write - Instinct router: require_license at router level + per-route role guards (read/propose → MEMBER, approve/reject/audit → ADMIN) - Agent knowledge mutations: require_agent_owner_or_admin (mirrors PATCH/DELETE agent CRUD, which already had this guard) 222 RBAC matrix + guards tests pass. |
||
|
|
88581e7022 |
fix(rbac): add missing RBAC guards to connector and upload mutation endpoints (#1058)
Connector mutations (execute/enable/disable/config) and upload writes
(POST /uploads, POST /uploads/folders) were only protected by
require_license, which checks plan validity but not workspace role.
- Add connector.execute (MEMBER) and connector.manage (ADMIN) to ACTIONS
- Add uploads.write (MEMBER) and uploads.manage (ADMIN) to ACTIONS
- Wire require_action_any_workspace on 4 connector mutation routes
- Wire require_action_any_workspace("uploads.write") on 2 upload POST routes
The RBAC matrix test auto-covers all 4 new entries (204 pass).
Fleet install was already fixed on 2026-04-19 — no change needed there.
|
||
|
|
bf94379e6e | feat: workspace invite uses token for nav link, group add creates in-app notification | ||
|
|
2cf5155045 | fix: group notification broken by unbound group_name reference | ||
|
|
0e8b69abaf | feat: notification system with room_id for navigation, DM and group chat notifications, missing endpoints | ||
|
|
c7daf8dfd9 | Merge branch 'ee' into feat/backend-ripple-manifest | ||
|
|
2b0c7ca06a |
feat(cloud): manifest-validated ripple writes + list-before-create gate
- Add list_pockets_for_agent and a pre-persist rippleSpec validator in ee/cloud/pockets/agent_context.py so the agent enforces list-before- create and catches prop-name drift against the widget manifest before the cloud writes the pocket. - Pull pocket interaction prompts from ee.ripple.get_pocket_prompts and delete the duplicated literal in ee/cloud/chat/agent_service.py (single source of truth in ee/ripple). - Add tests for the prompt-source guard and the new agent-context helpers; document the resolver plan under docs/plans/. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b5c7a0e46f |
refactor(chat): import inline prompt from ee.ripple instead of literal
Replaces the ~160-line _RIPPLE_HINT literal in agent_service.py with an import of INLINE_RIPPLE_SYSTEM_PROMPT from ee.ripple._inline. The chat-inline system prompt now lives in exactly one place. Tests: test_build_context_block_includes_ripple_hint fails as expected (asserts buttons forbidden, but the new prompt documents chat.send round-trip with buttons). Task 4 rewrites the test to match the new contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
0bd2f50977 |
feat(prompts): UI-first language + composition cookbook + typed-widgets nudge
Pushes agent toward ui-spec-by-default for structured answers (status, KPI, list, comparison, code+explanation, link, trend, breakdown, steps, pros/cons, citations). Adds 14-recipe composition cookbook in the chat-inline ripple hint. The pocket-creation widget context now nudges agents to compose with typed widgets (kanban, gantt, stat, chart, link-preview) over rebuilding from flex+text. Note: agent_service.py's _RIPPLE_HINT will be deleted in the next refactor; this commit is a checkpoint preserving the intermediate work. |
||
|
|
111658689e |
refactor(clients): rename src/pocketpaw/integrations/ → src/pocketpaw/clients/ (#1053)
The previous "integrations" name was vague — it could mean "external
service integrations" or "API integrations" or "ee/cloud integrations."
"clients" is the actual role: HTTP / SDK clients for third-party
services (Gmail, Google Calendar, Google Docs, Google Drive, Reddit,
Spotify) plus shared OAuth + token storage.
Rename only — zero behaviour change. Every import site rewritten via
sed; doc references updated; ruff clean.
Layer responsibilities (now explicit):
- src/pocketpaw/clients/ HTTP / SDK clients (low-level: tokens,
MIME, base64, HTTP calls). Stateful.
- src/pocketpaw/connectors/ Connector protocol + adapters wrapping
clients. Stateless.
- src/pocketpaw/tools/builtin/ Agent-facing tools. Hand-tuned LLM
response formatting.
- ee/cloud/connectors/ Tenanted state + REST router (only
enterprise piece).
Tests
- 199 connector + integration tests pass (199 / 199, no regressions)
- ruff check clean on src/pocketpaw/, ee/cloud/connectors/, tests/connectors/
What's NOT in this PR
- pocketpaw/api/v1/oauth_integrations.py kept as-is — different concept
(REST endpoint for OAuth flows, not a service client). Could rename
later but not load-bearing.
|
||
|
|
a1a5203410 |
feat(connectors): CLI adapters adopt the protocol + local-agent bus listener (#1052)
Phase 1 PR-8. Wires the cross-process dispatch contract for CLI connectors (firebase, gcp, future kubectl/gh/aws/...) so the cloud router can hand local-mode actions off to the user's pocketpaw runtime. What landed - src/pocketpaw/connectors/firebase_adapter.py — actions() now stamps every schema with execution_mode=LOCAL + requires_binary="firebase". widgets() returns [] (admin ops, no default home widgets). health() runs `firebase --version` for a cheap reachability probe. - src/pocketpaw/connectors/gcp_adapter.py — same treatment, binary is "gcloud". Reuses the _local_action helper added to firebase. - src/pocketpaw/runtime/connector_bus.py — new listener subscribing to connector.exec.requested. Looks up the adapter, runs it on the local host, publishes connector.exec.completed. Fails fast on missing binary (connector.binary_missing), unknown connector (connector.not_found), timeout, malformed payload. - ee/cloud/__init__.py — register_listener() called from mount_cloud() so the in-process round-trip works in single-user pocketpaw mode. Cross-process caveat - The bus is in-process today (ee.cloud.shared.events.event_bus). In single-user pocketpaw deployments cloud + runtime live in the same FastAPI app — round-trip is direct. - Multi-tenant cloud needs RedisBus (Task 33) for cross-host dispatch. The contract here is unchanged — only the transport swaps. The cloud router still 503s when a local-mode action is invoked without a listener responding (the await-with-correlation await pattern lands alongside RedisBus). Tests (5 new) - tests/connectors/test_connector_bus.py: - register_is_idempotent - round_trip_runs_adapter_and_publishes_completed - missing_binary_fails_fast - unknown_connector_returns_not_found - malformed_payload 179 connector-related tests pass across tests/connectors, tests/cloud (router + execute + e2e + gcp + firebase). ruff clean on every changed file. What's NOT in this PR - RedisBus / cross-process transport — Task 33 - Cloud router awaiting connector.exec.completed with a request_id correlation — depends on the persistent transport above. Cloud still 503s for local-mode actions; on the local-mounted shape that's documented as a Phase 1 limitation. |
||
|
|
95117aea77 |
feat(connectors): GmailConnector — first native adapter on the protocol (#1047)
Phase 1 PR-3. Reference implementation that proves the protocol shape works for a real production connector. Lands the catalog entry + native Python adapter wrapping the existing GmailClient + 3 home widget recipes (Inbox / Important Emails / Email Stats) + snapshot tests pinning the action surface. Stacks on PR #1046 (protocol additions). What landed - connectors/gmail.yaml — catalog metadata + 9-action manifest (8 mirroring existing tools + gmail_summary for the Email Stats widget) - src/pocketpaw/connectors/adapters/__init__.py — new namespace for native Python adapters - src/pocketpaw/connectors/adapters/gmail.py — GmailConnector wraps the existing GmailClient (OAuth refresh, MIME, base64 stay there). Implements the full protocol: connect, disconnect, actions, execute, sync, schema, widgets, health. - registry.py — _create_native_adapter("gmail") returns GmailConnector. Adds _NATIVE_COMM_CONNECTORS set so Calendar/Docs/Drive plug in the same way in PR-4..6. - ee/cloud/connectors/service._adapter_for_definition prefers the native adapter when one exists; falls back to DirectRESTAdapter. Widget recipes - Inbox: feed, gmail_search "is:unread" - Important Emails: feed, gmail_search "is:important newer_than:1d" - Email Stats: stats, gmail_summary (aggregates unread / today / avg) Tests (14 new + 1 cloud integration) - tests/connectors/test_gmail_connector.py — metadata, action surface snapshot (8 names match tools/builtin/gmail.py), trust levels, cloud-mode invariant, widget recipes, health up/down, execute() delegation to GmailClient, registry wiring - tests/cloud/test_connectors_execute.py — gmail enabled → /widget-recipes returns 3 Gmail rows with the expected titles What's NOT in this PR - Replacing the 8 hand-written tool classes in tools/builtin/gmail.py. Those have hand-tuned LLM-friendly response formatting that a generic connector_tools_for(c) generator can't reproduce verbatim. A future PR (3.5+) introduces a per-action formatter abstraction before the swap. The snapshot test in test_gmail_connector.py pins the names so the swap is byte-identical when it lands. - Calendar / Docs / Drive / Reddit / Spotify migration → PR-4 through PR-8 follow this same pattern (catalog YAML + native adapter + widget recipes + snapshot tests). Tests - 50 new + regression tests pass: 14 GmailConnector tests, 16 protocol additions tests, 9 cloud execute tests, 12 PR-1 contract tests. - 173 connector-related tests across tests/connectors, tests/cloud (router + execute + e2e + gcp + firebase), tests/v1, tests/test_gmail pass. - ruff clean on every changed file. |
||
|
|
f901e2eba5 |
feat(connectors): protocol additions — widgets, health, scope, execution mode (#1046)
Phase 1 PR-2. Adds the protocol surface the home widget consumer
(picker rail) and CLI connectors (firebase, gcp, gh, kubectl) need
without requiring a per-connector code rewrite. Lands the cloud
router's mode-aware dispatch contract so PR-9 has a clean target to
plug the runtime listener into.
Protocol surface (src/pocketpaw/connectors/protocol.py)
- ExecutionMode StrEnum: CLOUD | LOCAL | SANDBOX
- ConnectorScope tagged union: PocketScope | WorkspaceScope | UserScope
(frozen dataclasses, kind discriminator)
- ActionSchema gains execution_mode (default CLOUD) and requires_binary
- ConnectorHealth dataclass — live status snapshot for the panel
- WidgetRecipe dataclass — pre-baked default home widget
- ConnectorProtocol gains widgets() and health() methods
DirectRESTAdapter defaults (src/pocketpaw/connectors/yaml_engine.py)
- widgets() returns [] — YAML connectors don't ship recipes in Phase 1
- health() reflects the current connect() state (cheap, no probe)
- actions() reads optional execution_mode + requires_binary from YAML
rows, falls back to CLOUD on garbage
Cloud router (ee/cloud/connectors)
- New DTOs: ExecuteActionRequest / ExecuteActionResponse / WidgetRecipeResponse
- service.list_widget_recipes(workspace_id) — flattens recipes across
every enabled connector, tenant-filtered, swallows per-adapter errors
- service.execute(workspace_id, name, body, user_id) — mode dispatch:
cloud → adapter.execute() in-process, returns 200 + result
local → emits connector.exec.requested on the bus, raises
CloudError(503, "connector.local_agent_unavailable")
until PR-9 lands the runtime listener
sandbox → CloudError(501, "connector.sandbox_not_implemented")
- New routes: GET /api/v1/cloud/connectors/widget-recipes,
POST /api/v1/cloud/connectors/{name}/execute
Tests (35 new)
- tests/connectors/test_protocol_widgets.py — 16 tests pinning every
new type, the YAML adapter defaults, and the YAML→ActionSchema
execution_mode read path
- tests/cloud/test_connectors_execute.py — 7 tests pinning mode
dispatch, the bus emit on local mode, 404s for unknown
connector / action, sandbox 501
Regressions
- 148 connector-related tests pass across tests/connectors,
tests/cloud (router + execute + e2e + gcp + firebase),
tests/v1 (legacy connector status). Zero behaviour change to the
legacy /api/v1/connectors path or to YAML connector execution.
- ruff clean on every changed file.
What's NOT in this PR
- Gmail adopting the protocol → PR-3
- firebase + gcp adapters rewritten with execution_mode=local → PR-9
- pocketpaw/runtime/connector_bus.py listener → PR-9 (the cross-process
one — until it lands, local-mode actions return 503 with a clear
"open your local PocketPaw" message)
|
||
|
|
e692b9bbb7 |
feat(connectors): cloud entity + workspace-scoped REST router (#1045)
* feat(connectors): cloud entity + workspace-scoped REST router (Phase 1 PR-1) First land of the connector layer Phase 1. Strategy is locked at ee/cloud/connectors/CHARTER.md: consolidate four scattered layers (YAML specs, src/pocketpaw/connectors runtime, integrations HTTP clients, tools/builtin agent tools) behind one protocol inside pocketpaw, then extract to paw-connectors/ as a workspace sibling once the protocol stabilizes (Phase 2, ~3-4 weeks out). This PR adds the tenanted state and the cloud REST router. No protocol changes yet, no behavior change to the existing four layers. What landed - ee/cloud/connectors/ following the 4-file shape: domain.py (WorkspaceConnector + AvailableConnector frozen dataclasses), dto.py (split request/response Pydantic models), service.py (module-level async API: list / get / enable / disable / update_config / record_sync), router.py (REST endpoints under /api/v1/connectors). - ee/cloud/models/connector.py — WorkspaceConnector Beanie document, one row per (workspace, name). - Registered in ALL_DOCUMENTS so init_beanie picks it up. - mount_cloud() includes the new router alongside pockets/agents/etc. - 12 contract tests covering list / enable / disable / config patch / detail, plus tenancy isolation and scope validation. Wire shape mirrors src/pocketpaw/api/v1/connectors.py:ConnectorInfo so the frontend's getConnectors() keeps working unchanged. The cloud handler shadows the runtime one at /api/v1/connectors via FastAPI mount order in cloud deployments; local-only pocketpaw keeps the v1 endpoint. Cloud rules followed (per pocketpaw/CLAUDE.md ee/cloud section) - entity has the 4-file shape with no repositories.py - writes go through service.py only; routers never import models - domain value objects are frozen with required workspace_id - DTOs split between request and response - service signature is async def op(workspace_id, body) -> response - body validated with model_validate at the entry of every write - every read filters by workspace_id (tenancy) - mapping done with from_attributes=True helpers in the service - every state-mutating function emits an event_bus event - errors raised as CloudError subclasses, never HTTPException What's NOT in this PR - Connector.widgets() and Connector.health() protocol additions — PR-2 - Gmail adopting the protocol as the reference implementation — PR-3 - Frontend changes — paw-enterprise's ConnectorPanel keeps reading /api/v1/connectors and naturally picks up the cloud handler - Token bytes in Mongo — stays local in token_store.py for Phase 1 - Calendar / Docs / Drive / Reddit / Spotify migration — follows the Gmail pattern in PR-4 through PR-8 Tests - 12 new contract tests pass - 1673 cloud tests pass (1 pre-existing failure unrelated to this work, test_agent_bridge_does_not_import_ws_manager_broadcast_directly hard-codes a Windows path) - 87 connector-related tests across v1 + cloud + e2e pass - ruff clean on every new file * fix(connectors): namespace cloud router under /api/v1/cloud/connectors The legacy pocket-scoped routes in src/pocketpaw/api/v1/connectors.py (connect / disconnect / execute / status) are still in active use by PocketDataPanel.svelte. Mounting the new cloud router at /api/v1/connectors shadowed the legacy GET endpoint via FastAPI's first-registered-wins behaviour (mount_cloud runs before mount_v1_routers per dashboard.py:214), so PocketDataPanel was returning workspace-level state instead of pocket-level when callers passed ?pocket_id=X. Move the cloud router to /api/v1/cloud/connectors so the two surfaces coexist: - /api/v1/connectors → legacy pocket-scoped, untouched - /api/v1/cloud/connectors → new workspace-scoped (this PR) The home-widget integration (PR-2 onward) calls the new path. Once PR-2 lands the protocol additions and the home consumer is wired, PocketDataPanel can migrate to the cloud entity in its own PR and the legacy path retires. Tests updated to hit the new path. 37 connector tests pass: 12 new contract tests, 12 legacy v1 status tests, 13 cloud connector tests. * docs(connectors): add ExecutionMode + local-agent bus to charter CLI-based connectors (firebase, gcp, gh, kubectl, etc.) cannot execute in the cloud's FastAPI process — there's no clean way to multi-tenant per-workspace gcloud configs on a shared host. Adds a second axis to the protocol so the runtime knows where each action is allowed to run. Locked decisions - ExecutionMode StrEnum on ActionSchema: cloud | local | sandbox - requires_binary field on ActionSchema (gcloud / firebase / gh / …) - Local mode flows through the existing chat WebSocket using two new bus topics: connector.exec.requested (cloud → agent), connector.exec.completed (agent → cloud). No new transport. - Sandbox mode is reserved in the enum but deferred until a Nerve client needs DB-CLI widgets running 24/7. Future PR is runtime-only, not a schema change. - Local mode requires the user's pocketpaw runtime to be online. Failure modes documented (timeout 503, missing binary, cloud offline). - YAML connectors default to cloud mode (no behaviour change). Firebase + GCP declare local mode per action with their binary. Charter additions - §4 protocol shape: ExecutionMode + requires_binary on ActionSchema - §6.2 new sub-section: CLI connectors and the local-agent bus - §8 migration: PR-8 adds the runtime bus listener at pocketpaw/runtime/connector_bus.py - §9 captain-resolved questions: local online constraint, sandbox deferral, bus reuses chat WS - §11 acceptance criteria expanded from 5 to 6 — adds the end-to-end CLI round-trip pin - Out of scope: ExecutionMode.SANDBOX runtime Plan file at ~/.claude/plans/playful-greeting-tower.md updated to reflect ExecutionMode in PR-2 and add PR-9 (firebase + gcp adopt the protocol with local mode). No code changes in this commit. Implementation lands in PR-2 (protocol) and PR-9 (firebase / gcp + bus listener). |