Repository navigation
Fix/kilo chat sandbox id migration tolerance - #3308
Conversation
| function candidateInstanceIdFromSandboxId(sandboxId: string): string | null { | ||
| if (!isInstanceKeyedSandboxId(sandboxId)) return null; | ||
| const derived = instanceIdFromSandboxId(sandboxId); | ||
| return isValidInstanceId(derived) ? derived : null; |
There was a problem hiding this comment.
WARNING: The secondary isValidInstanceId re-validation is correct and necessary, but the comment above it says isInstanceKeyedSandboxId "only checks prefix + total length" — which is accurate. However, worth noting: instanceIdFromSandboxId will format any 32-character string into UUID shape regardless of whether those characters are valid hex. isValidInstanceId then re-checks with a strict hex UUID regex. This defense-in-depth is good.
One edge case to be aware of: if sandboxId is exactly ki_ followed by 32 non-hex chars (e.g. ki_zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz), instanceIdFromSandboxId returns a UUID-shaped string with non-hex chars (e.g. zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz), and isValidInstanceId correctly returns false, so null is returned. The guard works — just confirming the reasoning is sound.
There was a problem hiding this comment.
Nothing to fix here.
| sandboxId: string | ||
| ): Promise<boolean> { | ||
| const db = getWorkerDb(connectionString); | ||
| const candidateInstanceId = candidateInstanceIdFromSandboxId(sandboxId); |
There was a problem hiding this comment.
WARNING: The test mock in sandbox-ownership.test.ts swallows the .where() clause entirely (the mock returns rows regardless of the predicate). This means the new candidateInstanceId OR-branch and the security guard (isValidInstanceId re-validation) have zero test coverage of their actual database query logic.
Consider adding tests that cover:
- A
ki_<valid-uuid-hex>sandboxId that should match viakiloclaw_instances.id(migration-tolerance path) - A
ki_<32-non-hex-chars>sandboxId (attacker-controlled input) — should not match even when the DB has a row - A legacy non-
ki_sandboxId — should only use thesandbox_idmatch branch
The current test structure mocks at too high a level to observe which WHERE predicate was actually constructed.
Code Review SummaryStatus: 1 Issue Remaining | Recommendation: Address before merge Executive SummaryThe test mock coverage gap (previous WARNING #2) has been resolved by the new commit — drizzle-orm operators are now properly stubbed as predicate functions and all Overview
Issue Details (click to expand)WARNING
Resolved Issues
Files Reviewed (2 files)
Fix these issues in Kilo Cloud Reviewed by claude-4.6-sonnet-20260217 · 676,526 tokens Review guidance: REVIEW.md from base branch |
Summary
Loosen sandbox ownership checks in kilo-chat to accept the instance keyed
ki_<uuid hex>sandboxId form when the database row still stores the legacy userId derived value. Restores chat for users whose kiloclaw Durable Object has moved to the new sandboxId format while theirkiloclaw_instances.sandbox_idrow has not. The same tolerance is applied to the companion owner lookup so bot status pushes and bot conversation creation also work for the same population.services/kilo-chat/src/services/sandbox-ownership.ts:candidateInstanceIdFromSandboxId(sandboxId)returns the instance UUID aki_form references, ornull. Revalidates withisValidInstanceIdbefore returning so non hex content cannot reach theuuidcolumn and produce a Postgres cast error (which would turn a 4xx into a 500 on caller controlled input).queryOwnsSandboxmatchesWHERE user_id = $userId AND destroyed_at IS NULL AND (sandbox_id = $sandboxId OR id = derivedInstanceId). Theuser_idanddestroyed_atfilters are unchanged.querySandboxOwner(behindlookupSandboxOwnerUserId) uses the same OR shape againstdestroyed_at IS NULL.services/kilo-chat/src/__tests__/sandbox-ownership.test.ts:vi.importActualand overrideseq,and,or,isNullwith predicate factories, so the WHERE expression is actually evaluated againstdbState.rows.ki_form viaid,ki_with non hex content,ki_referencing another user,ki_matching a destroyed row, legacysandbox_idbelonging to another user, and the existingdestroyed_atcache invalidation paths.Verification
kiloclaw_instances.sandbox_idis in legacy form and whose DO reports aki_<hex>value can load/claw/chatand create a conversation without a 403.Visual Changes
N/A
Reviewer Notes
Ownership scope is intentionally unchanged. The relaxation only adds a second accept form for the same row, derived deterministically from the row's own UUID.
queryOwnsSandboxstill filters byuser_idfrom the verified bearer, so a caller cannot use theki_form to reach another user's row. The derived UUID is validated before reaching theuuidcolumn, so length 35 strings with non hex content cannot produce a cast error.The legacy form derived from
userIdis intentionally not accepted as a third form. That would still be ownership scoped, but would let downstream code that uses the caller provided sandboxId as a bot identifier (bot:kiloclaw:${sandboxId}) split conversations across two sandbox buckets if some clients send legacy and others sendki_.The predicate driven test mock evaluates drizzle's WHERE expression in JavaScript so the new OR branch and the
isValidInstanceIdguard are actually exercised rather than skipped by the previous mock.