Skip to content

Commit c241a9b

Browse files
committed
fix(kiloclaw): keep new Composio settings unmanaged
1 parent be07c6b commit c241a9b

6 files changed

Lines changed: 16 additions & 108 deletions

File tree

.specs/kiloclaw-composio.md

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,17 @@ Kilo previously shipped a managed Composio onboarding experiment that created Ki
5353
13. Kilo MUST NOT create new managed Composio identities, Connect Links, connected-account onboarding flows, or managed credential injection for KiloClaw onboarding.
5454
14. Direct Google Calendar onboarding, when offered, is independent of Composio and MUST NOT depend on retired managed Composio state.
5555
15. Retired managed Composio identities MUST NOT be reused for new instances or configuration updates.
56-
16. After managed creation paths are disabled, the system MUST verify whether an active instance associated with retired managed identity state retains managed Composio credentials, including a possible partial-write case where runtime injection succeeded before a tracking marker was persisted.
57-
17. Any confirmed managed credential material in a live instance MUST be cleared before obsolete stored managed identity state is deleted. Verification and clearing MUST NOT remove manually configured Composio credentials.
58-
18. Until obsolete managed-state schema is dropped, an instance that receives user-provided Composio credentials through provision or Settings MUST be recorded as manually configured, and an instance that clears both Composio fields MUST clear that provenance marker. This marker retention is cleanup safety metadata only and MUST NOT restore managed onboarding behavior.
59-
19. If no live managed runtime credential remains, obsolete managed identity rows, encrypted credential residue, connected-account identifiers, and destroyed-instance tracking markers MAY be removed by dropping the retired managed-state schema.
60-
20. Obsolete managed-state database structures MUST NOT be dropped until managed creation is disabled and live runtime residue has been ruled out or cleared.
56+
16. After managed creation paths are disabled, the system MUST verify whether any existing live instance retains managed Composio credentials before obsolete stored managed identity state is deleted.
57+
17. Any confirmed managed credential material in an existing live instance MUST be cleared before obsolete stored managed identity state is deleted. Verification and clearing MUST NOT remove manually configured Composio credentials.
58+
18. If no live managed runtime credential remains, obsolete managed identity rows, encrypted credential residue, connected-account identifiers, and destroyed-instance tracking markers MAY be removed by dropping the retired managed-state schema.
59+
19. Obsolete managed-state database structures MUST NOT be dropped until managed creation is disabled and live runtime residue has been ruled out or cleared.
6160

6261
### Credential Boundary and Data Protection
6362

64-
21. Kilo central or retired managed Composio credentials MUST NOT be injected into a user or organization OpenClaw instance.
65-
22. Logs, analytics, audit records, Sentry events, command output, and user-facing errors MUST NOT include raw Composio credentials, OAuth tokens, Connect Links containing secret material, or decrypted stored identity data.
66-
23. User-provided Composio secrets MUST continue to follow the normal KiloClaw secret encryption, transport, and deletion rules.
67-
24. Retired managed rows containing encrypted credentials or user-linked provider identifiers MUST be deleted after the required live-runtime verification or otherwise scrubbed in accordance with account-deletion requirements.
63+
20. Kilo central or retired managed Composio credentials MUST NOT be injected into a user or organization OpenClaw instance.
64+
21. Logs, analytics, audit records, Sentry events, command output, and user-facing errors MUST NOT include raw Composio credentials, OAuth tokens, Connect Links containing secret material, or decrypted stored identity data.
65+
22. User-provided Composio secrets MUST continue to follow the normal KiloClaw secret encryption, transport, and deletion rules.
66+
23. Retired managed rows containing encrypted credentials or user-linked provider identifiers MUST be deleted after the required live-runtime verification or otherwise scrubbed in accordance with account-deletion requirements.
6867

6968
## Error Handling
7069

@@ -79,7 +78,7 @@ Kilo previously shipped a managed Composio onboarding experiment that created Ki
7978

8079
- Removed managed identity provisioning, managed Connect Link onboarding, and managed callback injection from supported product behavior.
8180
- Retained explicit user-provided Composio credentials through Settings and the encrypted secret pipeline.
82-
- Added post-deploy live-runtime verification, temporary manual provenance tracking, and subsequent stored-state removal requirements for managed credentials created or injected while the experiment was shipped.
81+
- Added post-deploy live-runtime verification and subsequent stored-state removal requirements for managed credentials created or injected while the experiment was shipped.
8382

8483
### 2026-05-20 -- Managed onboarding experiment
8584

apps/web/src/lib/kiloclaw/instance-registry.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -196,22 +196,6 @@ export async function restoreDestroyedInstance(instanceId: string): Promise<void
196196
.where(eq(kiloclaw_instances.id, instanceId));
197197
}
198198

199-
// Retained until retired managed Composio state is verified and its schema is dropped.
200-
// Manual provenance prevents follow-up cleanup from removing user-owned secrets.
201-
export async function markComposioInstanceConfigManual(instanceId: string): Promise<void> {
202-
await db
203-
.update(kiloclaw_instances)
204-
.set({ composio_config_source: 'manual' })
205-
.where(eq(kiloclaw_instances.id, instanceId));
206-
}
207-
208-
export async function clearComposioInstanceConfigSource(instanceId: string): Promise<void> {
209-
await db
210-
.update(kiloclaw_instances)
211-
.set({ composio_config_source: null })
212-
.where(eq(kiloclaw_instances.id, instanceId));
213-
}
214-
215199
/**
216200
* Fetch the user's active personal KiloClaw instance (read-only, no upsert).
217201
*

apps/web/src/lib/kiloclaw/provision-secrets.test.ts

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,7 @@ jest.mock('@/lib/kiloclaw/encryption', () => ({
22
encryptKiloClawSecret: jest.fn((value: string) => `encrypted:${value}`),
33
}));
44

5-
import {
6-
encryptProvisionSecretsForWorker,
7-
getComposioSecretsPatchSource,
8-
hasComposioProvisionSecrets,
9-
} from './provision-secrets';
5+
import { encryptProvisionSecretsForWorker } from './provision-secrets';
106

117
describe('encryptProvisionSecretsForWorker', () => {
128
it('maps valid manual Composio secret keys to worker env var names before encrypting', () => {
@@ -46,25 +42,3 @@ describe('encryptProvisionSecretsForWorker', () => {
4642
}
4743
);
4844
});
49-
50-
describe('Composio manual source tracking', () => {
51-
it('identifies provision inputs that require a manual source marker', () => {
52-
expect(
53-
hasComposioProvisionSecrets({
54-
composioUserApiKey: 'uak_manual_credential_123',
55-
composioOrg: 'org-1',
56-
})
57-
).toBe(true);
58-
expect(hasComposioProvisionSecrets({ CUSTOM_SECRET: 'kept' })).toBe(false);
59-
});
60-
61-
it('marks manual patch updates and clears source only when both fields are removed', () => {
62-
expect(getComposioSecretsPatchSource({ composioUserApiKey: 'new-value' })).toBe(
63-
'upsert_manual'
64-
);
65-
expect(getComposioSecretsPatchSource({ composioUserApiKey: null, composioOrg: null })).toBe(
66-
'clear'
67-
);
68-
expect(getComposioSecretsPatchSource({ CUSTOM_SECRET: null })).toBe('none');
69-
});
70-
});

apps/web/src/lib/kiloclaw/provision-secrets.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,8 @@ import { encryptKiloClawSecret } from '@/lib/kiloclaw/encryption';
88

99
const COMPOSIO_SECRET_FIELD_KEYS = ['composioUserApiKey', 'composioOrg'] as const;
1010

11-
export function hasComposioProvisionSecrets(secrets: Record<string, string> | undefined): boolean {
12-
return COMPOSIO_SECRET_FIELD_KEYS.some(key => secrets?.[key] !== undefined);
13-
}
14-
15-
export function getComposioSecretsPatchSource(
16-
secrets: Record<string, string | null>
17-
): 'upsert_manual' | 'clear' | 'none' {
18-
const touchedValues = COMPOSIO_SECRET_FIELD_KEYS.filter(key => secrets[key] !== undefined).map(
19-
key => secrets[key]
20-
);
21-
if (touchedValues.length === 0) return 'none';
22-
if (touchedValues.every(value => value === null)) return 'clear';
23-
return 'upsert_manual';
11+
function hasComposioProvisionSecrets(secrets: Record<string, string>): boolean {
12+
return COMPOSIO_SECRET_FIELD_KEYS.some(key => secrets[key] !== undefined);
2413
}
2514

2615
function validateComposioProvisionSecrets(secrets: Record<string, string>): void {

apps/web/src/routers/kiloclaw-router.ts

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,9 @@ import {
6464
getInboundEmailAddressForInstance,
6565
} from '@/lib/kiloclaw/inbound-email-alias';
6666
import {
67-
clearComposioInstanceConfigSource,
6867
getActiveInstance,
6968
listAllActiveInstances,
7069
markActiveInstanceDestroyed,
71-
markComposioInstanceConfigManual,
7270
renameInstance,
7371
restoreDestroyedInstance,
7472
workerInstanceId,
@@ -78,11 +76,7 @@ import {
7876
getPersonalProvisionLockKey,
7977
withKiloclawProvisionContextLock,
8078
} from '@/lib/kiloclaw/provision-lock';
81-
import {
82-
encryptProvisionSecretsForWorker,
83-
getComposioSecretsPatchSource,
84-
hasComposioProvisionSecrets,
85-
} from '@/lib/kiloclaw/provision-secrets';
79+
import { encryptProvisionSecretsForWorker } from '@/lib/kiloclaw/provision-secrets';
8680
import {
8781
clearSubscriptionLifecycleAfterInstanceDestroy,
8882
clearTrialInactivityStopAfterStart,
@@ -1139,10 +1133,6 @@ async function provisionInstance(
11391133
: undefined
11401134
);
11411135

1142-
if (hasComposioProvisionSecrets(input.secrets)) {
1143-
await markComposioInstanceConfigManual(result.instanceId);
1144-
}
1145-
11461136
return result;
11471137
}
11481138

@@ -3463,18 +3453,11 @@ export const kiloclawRouter = createTRPCRouter({
34633453
const instance = await getActiveInstance(ctx.user.id);
34643454
const client = new KiloClawInternalClient();
34653455
try {
3466-
const result = await client.patchSecrets(
3456+
return await client.patchSecrets(
34673457
ctx.user.id,
34683458
{ secrets: encryptedPatch, meta: input.meta },
34693459
workerInstanceId(instance)
34703460
);
3471-
const composioSourceAction = getComposioSecretsPatchSource(secrets);
3472-
if (instance && composioSourceAction === 'upsert_manual') {
3473-
await markComposioInstanceConfigManual(instance.id);
3474-
} else if (instance && composioSourceAction === 'clear') {
3475-
await clearComposioInstanceConfigSource(instance.id);
3476-
}
3477-
return result;
34783461
} catch (err) {
34793462
if (err instanceof KiloClawApiError && err.statusCode >= 400 && err.statusCode < 500) {
34803463
// Extract message from worker response body (JSON or plain text)

apps/web/src/routers/organizations/organization-kiloclaw-router.ts

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,8 @@ import {
3939
getInboundEmailAddressForInstance,
4040
} from '@/lib/kiloclaw/inbound-email-alias';
4141
import {
42-
clearComposioInstanceConfigSource,
4342
getActiveOrgInstance,
4443
markActiveInstanceDestroyed,
45-
markComposioInstanceConfigManual,
4644
renameOrgInstance,
4745
restoreDestroyedInstance,
4846
workerInstanceId,
@@ -52,11 +50,7 @@ import {
5250
getOrganizationProvisionLockKey,
5351
withKiloclawProvisionContextLock,
5452
} from '@/lib/kiloclaw/provision-lock';
55-
import {
56-
encryptProvisionSecretsForWorker,
57-
getComposioSecretsPatchSource,
58-
hasComposioProvisionSecrets,
59-
} from '@/lib/kiloclaw/provision-secrets';
53+
import { encryptProvisionSecretsForWorker } from '@/lib/kiloclaw/provision-secrets';
6054
import {
6155
organizationMemberProcedure,
6256
organizationMemberMutationProcedure,
@@ -487,10 +481,6 @@ export const organizationKiloclawRouter = createTRPCRouter({
487481
{ orgId: input.organizationId }
488482
);
489483

490-
if (hasComposioProvisionSecrets(input.secrets)) {
491-
await markComposioInstanceConfigManual(result.instanceId);
492-
}
493-
494484
PostHogClient().capture({
495485
distinctId: ctx.user.google_user_email,
496486
event: 'claw_org_instance_provisioned',
@@ -544,10 +534,6 @@ export const organizationKiloclawRouter = createTRPCRouter({
544534
{ instanceId: instance.id, orgId: input.organizationId }
545535
);
546536

547-
if (hasComposioProvisionSecrets(input.secrets)) {
548-
await markComposioInstanceConfigManual(result.instanceId);
549-
}
550-
551537
return result;
552538
}),
553539

@@ -751,18 +737,11 @@ export const organizationKiloclawRouter = createTRPCRouter({
751737
const instance = await requireOrgInstance(ctx.user.id, input.organizationId);
752738
const client = new KiloClawInternalClient();
753739
try {
754-
const result = await client.patchSecrets(
740+
return await client.patchSecrets(
755741
ctx.user.id,
756742
{ secrets: encryptedPatch, meta: input.meta },
757743
workerInstanceId(instance)
758744
);
759-
const composioSourceAction = getComposioSecretsPatchSource(input.secrets);
760-
if (composioSourceAction === 'upsert_manual') {
761-
await markComposioInstanceConfigManual(instance.id);
762-
} else if (composioSourceAction === 'clear') {
763-
await clearComposioInstanceConfigSource(instance.id);
764-
}
765-
return result;
766745
} catch (err) {
767746
if (err instanceof KiloClawApiError && err.statusCode >= 400 && err.statusCode < 500) {
768747
let message = `Secret patch failed (${err.statusCode})`;

0 commit comments

Comments
 (0)