-
Notifications
You must be signed in to change notification settings - Fork 37
feat(kiloclaw): add org-scoped instance management + per-instance Fly apps #1815
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
7518955
feat(kiloclaw): add org-scoped instance management + per-instance Fly…
pandemicsyn 1a191da
Merge branch 'main' into florian/feat/orgs
pandemicsyn 66ce92d
chore: remove .plan files from tracking, add to gitignore
pandemicsyn df42ad6
fix(kiloclaw): address review feedback — tab URL, error codes, cleanu…
pandemicsyn fbd62cf
fix(kiloclaw): thread instanceId through access gateway for org insta…
pandemicsyn f899eb4
fix(kiloclaw): use instance-keyed app name in postgres restore fallback
pandemicsyn 99d2661
fix(kiloclaw): scope CLI runs to instance_id for org isolation
pandemicsyn fa7ce01
fix(kiloclaw): add instance ownership checks in access gateway
pandemicsyn f9d831b
fix: remove unused useClawContext import from SettingsTab
pandemicsyn 8ff6bd2
fix(kiloclaw): use requireOrgInstance for config/catalog queries
pandemicsyn d0b1623
fix(kiloclaw): scope version pins per instance
pandemicsyn 630a4b9
fix(kilo-app): handle instance-scoped version pins
pandemicsyn c6dc69a
fix(kiloclaw): pass instance-id to google-setup container in org context
pandemicsyn 431a93f
feat(google-setup): support --instance-id for org instance targeting
pandemicsyn 061e23f
fix(kiloclaw): scope personal CLI runs by instance_id
pandemicsyn c10d31b
fix(kiloclaw): admin pin operations resolve instance from userId
pandemicsyn b4683fc
fix(kiloclaw): fix pin migration safety and self-pin detection
pandemicsyn 16f473a
fix(kiloclaw): admin instance detail pins target viewed instance
pandemicsyn 2843fe8
fix(kiloclaw): simplify pin migration — drop column + add instead of …
pandemicsyn 7751f77
fix(kiloclaw): cookie-based instance routing for catch-all proxy
pandemicsyn 6cc4889
fix(kiloclaw): add destroy/restore guards and lint fixes to cookie-ro…
pandemicsyn 65ad283
fix(kiloclaw): org getMyPin returns null when no instance exists
pandemicsyn 58f3cda
fix(kiloclaw): route admin and org endpoints to correct instance
pandemicsyn c3cceb9
fix(kiloclaw): prevent silent fallback to personal instance
pandemicsyn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,9 @@ import { setAppSecret } from '../fly/secrets'; | |
| import { generateEnvKey } from '../utils/env-encryption'; | ||
| import { METADATA_KEY_USER_ID } from './machine-config'; | ||
|
|
||
| /** UUID v4 pattern — used to detect instance-keyed ownerKeys. */ | ||
| const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; | ||
|
|
||
| // -- Persisted state schema -- | ||
|
|
||
| const AppStateSchema = z.object({ | ||
|
|
@@ -80,32 +83,43 @@ export class KiloClawApp extends DurableObject<KiloClawEnv> { | |
| } | ||
|
|
||
| /** | ||
| * Ensure a Fly App exists for this user with IPs allocated and env key set. | ||
| * Ensure a Fly App exists for this owner with IPs allocated and env key set. | ||
| * Idempotent: creates the app only if it doesn't exist yet. | ||
| * Returns the app name for callers to cache. | ||
| * | ||
| * @param ownerKey - Either a userId (personal) or `"org:{orgId}"` (org). | ||
| * Org keys derive an `oapp-{hash}` app name instead of `acct-{hash}`. | ||
| */ | ||
| async ensureApp(userId: string): Promise<{ appName: string }> { | ||
| async ensureApp(ownerKey: string): Promise<{ appName: string }> { | ||
| await this.loadState(); | ||
|
|
||
| if (this.userId && this.userId !== userId) { | ||
| throw new Error(`userId mismatch: DO has ${this.userId}, caller passed ${userId}`); | ||
| if (this.userId && this.userId !== ownerKey) { | ||
| throw new Error(`ownerKey mismatch: DO has ${this.userId}, caller passed ${ownerKey}`); | ||
| } | ||
|
|
||
| const apiToken = this.env.FLY_API_TOKEN; | ||
| if (!apiToken) throw new Error('FLY_API_TOKEN is not configured'); | ||
| const orgSlug = this.env.FLY_ORG_SLUG; | ||
| if (!orgSlug) throw new Error('FLY_ORG_SLUG is not configured'); | ||
|
|
||
| // Derive app name (deterministic from userId, with env prefix in dev) | ||
| // Derive app name based on ownerKey type: | ||
| // - UUID (instanceId) → inst-{hash} (per-instance app) | ||
| // - userId string → acct-{hash} (legacy per-user app) | ||
| const prefix = this.env.WORKER_ENV === 'development' ? 'dev' : undefined; | ||
| const appName = this.flyAppName ?? (await apps.appNameFromUserId(userId, prefix)); | ||
|
|
||
| // Persist userId + appName early so we can retry on partial failure | ||
| const isInstanceKeyed = UUID_RE.test(ownerKey); | ||
| const appName = this.flyAppName | ||
| ? this.flyAppName | ||
| : isInstanceKeyed | ||
| ? await apps.appNameFromInstanceId(ownerKey, prefix) | ||
| : await apps.appNameFromUserId(ownerKey, prefix); | ||
|
|
||
| // Persist ownerKey + appName early so we can retry on partial failure. | ||
| // The `userId` storage field stores the ownerKey (historical name). | ||
| if (!this.userId || !this.flyAppName) { | ||
| this.userId = userId; | ||
| this.userId = ownerKey; | ||
| this.flyAppName = appName; | ||
| await this.ctx.storage.put({ | ||
| userId, | ||
| userId: ownerKey, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So |
||
| flyAppName: appName, | ||
| } satisfies Partial<AppState>); | ||
| } | ||
|
|
@@ -115,8 +129,8 @@ export class KiloClawApp extends DurableObject<KiloClawEnv> { | |
| if (!this.ipv4Allocated || !this.ipv6Allocated) { | ||
| const existing = await apps.getApp({ apiToken }, appName); | ||
| if (!existing) { | ||
| await apps.createApp({ apiToken }, appName, orgSlug, userId, METADATA_KEY_USER_ID); | ||
| console.log('[AppDO] Created Fly App:', appName); | ||
| await apps.createApp({ apiToken }, appName, orgSlug, ownerKey, METADATA_KEY_USER_ID); | ||
| console.log('[AppDO] Created Fly App:', appName, 'owner:', ownerKey); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -139,7 +153,7 @@ export class KiloClawApp extends DurableObject<KiloClawEnv> { | |
| // Step 4: Generate and store env encryption key if not done. | ||
| // Uses the same locked path as ensureEnvKey() to prevent interleaving. | ||
| if (!this.envKeySet) { | ||
| await this.ensureEnvKey(userId); | ||
| await this.ensureEnvKey(ownerKey); | ||
| } | ||
| } catch (err) { | ||
| // Partial state persisted above — arm a retry alarm so the DO self-heals | ||
|
|
@@ -155,15 +169,15 @@ export class KiloClawApp extends DurableObject<KiloClawEnv> { | |
| } | ||
|
|
||
| /** | ||
| * Get the env encryption key for this user's app. | ||
| * Enforces userId match to prevent cross-user key fetches. | ||
| * Get the env encryption key for this owner's app. | ||
| * Enforces ownerKey match to prevent cross-owner key fetches. | ||
| * Returns null if key hasn't been set yet. | ||
| */ | ||
| async getEnvKey(userId: string): Promise<string | null> { | ||
| async getEnvKey(ownerKey: string): Promise<string | null> { | ||
| await this.loadState(); | ||
|
|
||
| if (this.userId && this.userId !== userId) { | ||
| throw new Error(`userId mismatch: DO has ${this.userId}, caller passed ${userId}`); | ||
| if (this.userId && this.userId !== ownerKey) { | ||
| throw new Error(`ownerKey mismatch: DO has ${this.userId}, caller passed ${ownerKey}`); | ||
| } | ||
|
|
||
| return this.envKey; | ||
|
|
@@ -182,11 +196,11 @@ export class KiloClawApp extends DurableObject<KiloClawEnv> { | |
| * Called by Instance DO at machine start time. This ensures legacy apps that | ||
| * were created before the encryption feature get their key on first start. | ||
| */ | ||
| async ensureEnvKey(userId: string): Promise<{ key: string; secretsVersion: number }> { | ||
| async ensureEnvKey(ownerKey: string): Promise<{ key: string; secretsVersion: number }> { | ||
| await this.loadState(); | ||
|
|
||
| if (this.userId && this.userId !== userId) { | ||
| throw new Error(`userId mismatch: DO has ${this.userId}, caller passed ${userId}`); | ||
| if (this.userId && this.userId !== ownerKey) { | ||
| throw new Error(`ownerKey mismatch: DO has ${this.userId}, caller passed ${ownerKey}`); | ||
| } | ||
|
|
||
| const apiToken = this.env.FLY_API_TOKEN; | ||
|
|
||
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
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the significance of
this.userId? It feels redundant to have boththis.userIdand theownerKey.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ownerKey here is the caller-supplied argument, this.userId is what's already stored. userId is actually a bit of a misnomer now - it should be
ownerKeymoving forward. I can probably migrate that key in a follow up. ownerKey is actually gonna be instanceId for new deploys (that DO should represent the one instance in our pg table)