Skip to content

feat(admin): internal bot users with short-lived token minting - #8088

Merged
iscekic merged 8 commits into
mainfrom
feat/internal-bot-users
Oct 19, 2026
Merged

iscekic merged 8 commits into
mainfrom
feat/internal-bot-users

Conversation

@iscekic

@iscekic iscekic commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Internal workloads such as the bouncer watch and the auto-routing benchmark currently call the gateway with tokens for a personal user. They then share that user's per-user gateway rate limit with everything else the user does. This PR lets each workload run as its own dedicated internal bot user, which gets its own limit.

  • Admin page /admin/internal-bot-users (Users section of the admin sidebar). It lists bot-internal-* users with their org and role. A form creates a user from a slug, a display name and an organization id.
  • Creation goes through ensureInternalBotUser in apps/web/src/lib/bot-users/bot-user-service.ts. The per-org product bots and internal bots now share one insertBotUser insert: is_bot, a random api_token_pepper, the providerHash safety identifiers, a bot_stripe_* customer id, and auto top-up off. The id is bot-internal-<slug> (slug ^[a-z0-9][a-z0-9-]{0,40}$) and the email is <slug>-internal-bot@kilocode.internal. The user is added to the org as member. Running it again reuses the existing user and membership and never duplicates them. An existing non-bot row with the same id is refused.
  • POST /api/internal/bot-token mints a one-hour token for an internal bot.
    • Auth is Authorization: Bearer <BOT_TOKEN_MINT_SECRET>, compared with timingSafeEqual. If the secret is unset or empty, the endpoint returns 401.
    • The body is { userId, organizationId }. Both fields are required.
    • Only users with the bot-internal- prefix are eligible. They must also have is_bot, no block, a non-empty pepper, and an owner or member role in the org. This keeps humans and the per-org product bots (bot-code-review-*, ...) out.
    • Status codes: 400 invalid body, 401 bad auth, 404 missing user or membership, 403 ineligible.
    • The token shape mirrors the existing benchmark mint. The modern payload has the kilo-api and kilo-gateway audiences, tokenPurpose: 'delegated-workload', and { tokenSource: 'internal-bot', organizationId, organizationRole }. The legacy branch is used while the family flag is off.
    • Neither the token nor the secret is logged.
  • Config: BOT_TOKEN_MINT_SECRET is declared in config.server next to MCP_CATALOG_TOKEN_SECRET, the other single-purpose mint secret. It is documented in ENVIRONMENT.md and .env.local.example, and docs/token-issuance-policy.md gets a row for the new issuer.

Flag key

The modern token shape is gated by a new resource-token family, internal-bot, which maps to INTERNAL_BOT_RESOURCE_TOKENS_ENABLED. The repo already gives each producer family its own default-off flag behind SHARED_RESOURCE_TOKENS_ENABLED (cloud-agent-next, chat, benchmark, ...), so this follows that pattern. Reusing benchmark would couple unrelated workloads, such as the bouncer watch, to the benchmark's rollout. While the flag is off, the endpoint issues the legacy shape, and the eligibility rules apply in both modes. Set the flag in production to issue the modern shape.

There is no generic admin audit-log table, only kiloclaw_admin_audit_logs for KiloClaw. Every admin.* tRPC call already emits an admin_access event through adminProcedure, so this PR adds no separate audit write.

Verification

  • apps/web Jest: src/app/api/internal/bot-token/route.test.ts (mocked DB, matching the existing internal-mint tests) and src/lib/bot-users/bot-user-service.test.ts (real Postgres) pass.
    • The route tests cover: a missing, wrong or non-bearer secret; an unset or empty secret (fail closed); a human id; a per-org product bot; is_bot=false; blocked_at or blocked_reason set; a null or blank pepper; a missing membership; a billing_manager role. The rejection cases run in both token modes.
    • They also cover a one-hour token for owner and member with the exact API and gateway audiences, and the legacy one-hour shape.
    • The service tests check that creation is idempotent, that a second org adds a membership without re-creating the user, that an existing role is kept, that a non-bot id conflict is refused, and that slug validation works.
  • packages/web-shared config.server.resource-tokens.test.ts passes with the new family.
  • apps/web typecheck, oxlint and oxfmt are clean.

Deploy notes

Nothing calls the endpoint until BOT_TOKEN_MINT_SECRET is set in the web environment. Before any workload switches to it, create the bot users (for example bot-internal-bouncer-watch and bot-internal-benchmark) from the new admin page.

Also in this PR

  • apps/web/tests/setup-smoke/profile.spec.ts waits for the profile card's current title, "Platform routing mode". An earlier change on main renamed the card, so the setup smoke fails on main too.

Comment thread docs/token-issuance-policy.md Resolved
@kilo-code-bot

kilo-code-bot Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

The incremental change updates the setup-smoke assertion to the current Platform routing mode title, which matches AutoRoutingModeCard.tsx:90; no new issues were found and the previously fixed findings remain resolved in the unchanged PR files.

Files Reviewed (1 file changed since prior review; 15 unchanged PR files carried forward)
  • apps/web/tests/setup-smoke/profile.spec.ts - 0 issues
Previous Review Summaries (3 snapshots, latest commit 72ea686)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 72ea686)

Status: No Issues Found | Recommendation: Merge

Executive Summary

The incremental change fixes the last finding: every refusal fixture now carries a valid pepper and asserts the specific refusal message, so each case fails only on the condition it names. No new issues in the changed test.

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 0
Files Reviewed (1 file changed since prior review)
  • apps/web/src/lib/bot-users/bot-user-service.test.ts - 0 issues

The other 14 PR files are unchanged since commit 8107a94a and were carried forward as verified.

Previous review (commit 8107a94)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

The previous findings are fixed at HEAD; one remaining test-quality gap leaves the new blocked-bot rejection assertions unable to fail for the condition they target.

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
apps/web/src/lib/bot-users/bot-user-service.test.ts 93 Blocked/soft-deleted fixtures rely on the default null pepper, so the pepper check masks a missing is_bot/block check
Files Reviewed (7 files)
  • apps/web/src/app/admin/components/InternalBotUsers.tsx - 0 issues
  • apps/web/src/app/api/internal/bot-token/route.ts - 0 issues
  • apps/web/src/app/api/internal/bot-token/route.test.ts - 0 issues
  • apps/web/src/lib/bot-users/bot-user-service.ts - 0 issues
  • apps/web/src/lib/bot-users/bot-user-service.test.ts - 1 issue
  • apps/web/src/routers/admin/internal-bot-users-router.ts - 0 issues
  • docs/token-issuance-policy.md - 0 issues

Fix these issues in Kilo Cloud

Previous review (commit 2e6628e)

Status: 7 Issues Found | Recommendation: Address before merge

Executive Summary

The token mint's auth, eligibility, and JWT claims are sound, but a non-UUID organizationId produces an uncaught 500 and the admin create path silently re-adopts blocked bots that can never mint a token.

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 5
Issue Details (click to expand)

WARNING

File Line Issue
apps/web/src/app/api/internal/bot-token/route.ts 41 organizationId is validated only as a non-empty string; the uuid column comparison raises 22P02 and returns 500 instead of 400
apps/web/src/lib/bot-users/bot-user-service.ts 225 Blocked/soft-deleted bot rows are reused without a block check, so create reports success for a bot that mint always rejects

SUGGESTION

File Line Issue
apps/web/src/lib/bot-users/bot-user-service.ts 230 Select-then-insert races on the user and membership unique constraints; concurrent creates fail with 500
apps/web/src/app/admin/components/InternalBotUsers.tsx 135 Status badge ignores blocked_reason, so reason-only/soft-deleted bots display as Active
apps/web/src/app/api/internal/bot-token/route.test.ts 32 DB mock discards where() predicates, leaving org scoping and the membership predicate untested
docs/token-issuance-policy.md 108 Audience column records mint authorization instead of the actual audiences; legacy tokens have no aud
apps/web/src/routers/admin/internal-bot-users-router.ts 32 Org existence check does not exclude soft-deleted organizations

Fix these issues in Kilo Cloud

Files Reviewed (16 files)
  • apps/web/src/app/api/internal/bot-token/route.ts - 1 issue
  • apps/web/src/app/api/internal/bot-token/route.test.ts - 1 issue
  • apps/web/src/lib/bot-users/bot-user-service.ts - 2 issues
  • apps/web/src/lib/bot-users/bot-user-service.test.ts - 0 issues
  • apps/web/src/lib/bot-users/types.ts - 0 issues
  • apps/web/src/app/admin/components/InternalBotUsers.tsx - 1 issue
  • apps/web/src/app/admin/internal-bot-users/page.tsx - 0 issues
  • apps/web/src/app/admin/components/AppSidebar.tsx - 0 issues
  • apps/web/src/routers/admin/internal-bot-users-router.ts - 1 issue
  • apps/web/src/routers/admin-router.ts - 0 issues
  • packages/web-shared/src/lib/config.server.ts - 0 issues
  • packages/web-shared/src/lib/config.server.resource-tokens.test.ts - 0 issues
  • docs/token-issuance-policy.md - 1 issue
  • ENVIRONMENT.md - 0 issues
  • .env.local.example - 0 issues
  • apps/web/src/app/api/internal/auto-routing-benchmark/token/route.ts - context only

Reviewed by deepseek-v4.1-flash-1017 · Input: 38.7K · Output: 4.2K · Cached: 289.9K

Review guidance: REVIEW.md from base branch main

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