Skip to content

feat(personal): /personal/entities/* proxy to wisdom-agents#37

Open
artugro wants to merge 4 commits intomainfrom
feat/personal-proxy
Open

feat(personal): /personal/entities/* proxy to wisdom-agents#37
artugro wants to merge 4 commits intomainfrom
feat/personal-proxy

Conversation

@artugro
Copy link
Copy Markdown
Collaborator

@artugro artugro commented Apr 23, 2026

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

  • `src/services/personal.py` — `PersonalAgentsClient`: typed httpx wrapper over the wisdom-agents API. Maps upstream 404/403/409/422 to NotFound/Forbidden/BadRequest. 5xx/connection failures → `AgentsUpstreamError` (502).
  • `src/routes/personal.py` — 9 JWT-protected routes:
    • CRUD: `POST/GET /personal/entities`, `GET/PATCH/DELETE /personal/entities/{name}`
    • Lifecycle: `POST /personal/entities/{name}/pause|resume`
    • Chat: `POST /personal/entities/{name}/messages` (blocks on entity reply), `GET /personal/entities/{name}/messages` (paginated history)
  • `src/core/settings.py` — adds:
    • `INTUNO_AGENTS_BASE_URL`
    • `INTUNO_AGENTS_API_KEY`
    • `INTUNO_AGENTS_TIMEOUT_SECONDS` (30s default)
    • `INTUNO_AGENTS_CHAT_TIMEOUT_SECONDS` (60s — chat waits on LLM)
    • `PERSONAL_FREE_TIER_ENTITY_CAP` (1; Pro handled in Phase 5)
  • `src/main.py` — mounts the router.

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

  • Routes register (9 under `/personal`)
  • `PersonalAgentsClient` smoke against a live `wisdom-agents`:
    • Alice creates entity → `owner_user_id` stamped from forwarded header
    • Bob lists → 0 entities (owner scoping holds)
    • Bob GETs Alice's entity → 403 → `ForbiddenException`
    • Duplicate create → 409 → `BadRequestException`
    • Delete round-trip clean
  • Full JWT → proxy → wisdom-agents integration test (deferred; user-signup round-trip belongs in CI)

Files

4 files, +362 / -0.

🤖 Generated with Claude Code

artugro and others added 2 commits April 23, 2026 16:31
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>
@artugro
Copy link
Copy Markdown
Collaborator Author

artugro commented Apr 23, 2026

Updated with feat(service-auth): get_current_user_or_service dep + AGENTS_SERVICE_API_KEY setting + 4 routes migrated (networks, channels, a2a, registry). Paired with wisdom-agents #79 (commit 604beae) and intuno-sdk #26. Closes #38.

artugro and others added 2 commits April 23, 2026 18:30
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant