feat(personal): /personal/entities/* proxy to wisdom-agents#37
Open
feat(personal): /personal/entities/* proxy to wisdom-agents#37
Conversation
Closes the auth-bridge gap on the wisdom side. Frontend calls wisdom
with a JWT; wisdom validates, enforces tier quotas, and forwards to
the private wisdom-agents service with shared API key + X-User-Id.
- src/services/personal.py — PersonalAgentsClient: thin typed wrapper
over the wisdom-agents HTTP API. Forwards with the shared secret +
trusted user_id header. Maps upstream 404/403/409/422 to NotFound /
Forbidden / BadRequest exceptions the route layer re-raises cleanly.
5xx / connection failures → AgentsUpstreamError (502).
- src/routes/personal.py — 9 JWT-protected routes under /personal:
- CRUD: POST/GET /entities, GET/PATCH/DELETE /entities/{name}
- Lifecycle: POST /entities/{name}/pause, /resume
- Chat: POST /entities/{name}/messages (synchronous; blocks on reply),
GET /entities/{name}/messages (paginated history with cursor)
- src/core/settings.py — adds INTUNO_AGENTS_BASE_URL,
INTUNO_AGENTS_API_KEY, INTUNO_AGENTS_TIMEOUT_SECONDS,
INTUNO_AGENTS_CHAT_TIMEOUT_SECONDS, PERSONAL_FREE_TIER_ENTITY_CAP.
- src/main.py — mounts the router.
Quota enforcement: POST /entities counts the user's existing entities
and rejects with 400 if the Free cap is reached. Pro/enterprise tier
bumps land with the user-plan model in Phase 5.
Smoke test (bridge verified end-to-end against a local wisdom-agents):
- Alice creates entity via PersonalAgentsClient → owner_user_id stamped
from the forwarded header
- Bob lists → 0 entities (owner scoping)
- Bob GETs alice's entity → 403 → ForbiddenException
- Duplicate create → 409 → BadRequestException
- Delete round-trips cleanly
Part of the Intuno Personal #62 work. Paired with wisdom-agents#77
which implements the X-User-Id trust model on the other side.
Addresses the auth-bridge gap flagged in planning.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an auth mode for wisdom-agents (and any future internal service)
to call network / registry / A2A routes on behalf of specific users
without holding user-scoped API keys. Mirrors the X-User-Id trust we
already accept from wisdom-agents going the other direction — closes
the symmetry.
core/settings.py
- AGENTS_SERVICE_API_KEY env setting (infra secret, not DB-backed)
core/auth.py
- get_current_user_or_service: new dep.
* X-Service-Key present → validate against env + require X-On-Behalf-Of
(UUID) + load the delegated user
* Otherwise fall through to the existing JWT/API-key path
* Wrong service key → 401; missing/invalid X-On-Behalf-Of → 400;
unknown/inactive delegated user → 401
Route migration (imports aliased `as get_current_user` so handler
bodies don't change — current_user kwarg still receives the User):
- src/network/routes/networks.py
- src/network/routes/channels.py
- src/network/a2a/routes.py
- src/routes/registry.py
Personal proxy hotfix
- services/personal.py: AgentsUpstreamError now subclasses HTTPException
so FastAPI surfaces it as a clean 502 instead of a 500 with a raw
traceback. Upstream 401s (usually an API-key-mismatch config bug)
now surface as a specific message.
Broker routes untouched (custom integration-tracking auth; follow-up if
we want service delegation for direct invokes too).
Added .gitignore entry for backups/ to prevent future DB-dump commits.
Closes #38.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 tasks
Collaborator
Author
|
Updated with feat(service-auth): |
Closes IntunoAI/wisdom-frontend#45 on the backend side. Model + migration - src/models/user_invite.py — UserInvite row (token, optional email lock, optional expiry, single- or multi-use counter, inviter_user_id FK, redeemed_* fields). Registered in models/__init__.py. - alembic/versions/..._user_invites.py — creates the table on top of the 4ea22876971b head. Schemas - src/schemas/invite.py — InviteCreate / InviteRedeem (validators for password length + email optionality) / InvitePreview / InviteResponse + the two response types for create (with share URL) and redeem (with JWT). Repo + service - src/repositories/invite.py — get_by_token, get_by_id, list (filterable by unredeemed_only + include_expired), create, mark_redeemed (atomic with use_count bump via UPDATE ... RETURNING), delete. - src/services/invite.py — InviteService with preview / redeem / admin CRUD. Typed error classes (InviteNotFoundError/ExpiredError/ ExhaustedError/EmailMismatchError/EmailTakenError) each carrying a .status_code that the route layer maps cleanly. Redeem path wraps the existing AuthService.register_user + create_access_token. Routes - src/routes/invite.py — /invites mount: * GET /invites/{token}/preview — public, returns email+inviter+expiry shape; maps typed errors to 404/410. * POST /invites/{token}/redeem — public, body has password+first/last name+optional email; returns {access_token, token_type}. Maps email-mismatch/taken → 400/409. * POST /invites — admin, creates + returns share URL. Requires X-Service-Key matching AGENTS_SERVICE_API_KEY. * GET /invites — admin list (filterable). * DELETE /invites/{id} — admin revoke. CLI - src/scripts/create_invite.py — minted at the DB layer (no service key needed on the host). Prints the share URL. URL assembly uses FRONTEND_BASE_URL if configured, falling back to BASE_URL. A dedicated frontend URL setting lands with the production deploy config. Smoke (local): - alembic upgrade head applies cleanly - python -m src.scripts.create_invite --note smoke-test prints a URL - InviteService.preview(token) returns the expected shape Routes registered (5): GET/POST /invites, GET+POST /invites/{token}/..., DELETE /invites/{id}. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The invite URL is the shareable secret; locking redemption by email added friction without much security gain. An invite with an email field now pre-fills the signup form but the user can change it. - InviteEmailMismatchError → InviteEmailRequiredError (the only real failure mode left: no email anywhere — neither body.email nor invite.email). - redeem() final_email = body.email ?? invite.email. If both missing, 400 with a clear message. No more "locked to a specific email" case. Behavior for operators: - CLI --email still stored as invite metadata (useful for "who did I send this to" + a destination when email notifications land). - Redeem flow is now: one URL + one email on submit. User can use a different email than what was on the invite if they want.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Auth bridge for Intuno Personal. Frontend authenticates to wisdom (JWT), wisdom validates + enforces quotas + forwards to the private `wisdom-agents` service with the shared API key and an `X-User-Id` header. The frontend never talks to wisdom-agents directly (it doesn't hold the shared secret).
Closes: #36 (this repo's [personal-proxy] ticket).
Paired with: IntunoAI/wisdom-agents#77 — the trust model on the other side. Both sides verified end-to-end.
What lands
Quota enforcement (MVP)
`POST /personal/entities` counts the user's existing entities and rejects with 400 if the Free cap is reached. Pro/Enterprise caps land with the user-plan model in Phase 5. Numbers configurable via env.
Test plan
Files
4 files, +362 / -0.
🤖 Generated with Claude Code