Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 74 additions & 1 deletion services/cloud-agent-next/src/session-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ vi.mock('./workspace.js', () => ({
const tokenMocks = vi.hoisted(() => ({
issueCloudAgentGitHubSessionCapability: vi.fn(),
issueCloudAgentGitLabSessionCapability: vi.fn(),
issueCloudAgentBitbucketSessionCapability: vi.fn(),
resolveCloudAgentGitHubAuthForRepo: vi.fn(),
resolveManagedBitbucketToken: vi.fn(),
resolveManagedGitLabToken: vi.fn(),
Expand Down Expand Up @@ -452,7 +453,8 @@ function createGitLabCodeReviewMetadata(): CloudAgentSessionState {

function createBitbucketMetadata(
isCodeReview: boolean,
orgId: string | null = '123e4567-e89b-12d3-a456-426614174030'
orgId: string | null = '123e4567-e89b-12d3-a456-426614174030',
credentialContainment?: CredentialContainment
): CloudAgentSessionState {
return parseSessionMetadata({
metadataSchemaVersion: 2,
Expand All @@ -462,6 +464,7 @@ function createBitbucketMetadata(
...(orgId ? { orgId } : {}),
createdOnPlatform: isCodeReview ? 'code-review' : 'cloud-agent-web',
},
...(credentialContainment ? { workspace: { credentialContainment } } : {}),
auth: {
kilocodeToken: 'kilo-token',
kiloSessionId: 'kilo-session',
Expand Down Expand Up @@ -757,6 +760,41 @@ describe('SessionService.prepareWorkspace', () => {
);
});

it('preserves the capability origin (no strip) for a contained cold Bitbucket review', async () => {
const session = createSession(false);
const sandbox = createSandbox(session);
const metadata = createBitbucketMetadata(true, '123e4567-e89b-12d3-a456-426614174030', {
github: false,
gitlab: false,
bitbucket: true,
kilocode: false,
});
tokenMocks.issueCloudAgentBitbucketSessionCapability.mockResolvedValue({
success: true,
value: {
capability: 'kbb1.opaque-capability',
gitUrl: 'https://bitbucket.org/acme-team/widgets.git',
},
});

await new SessionService().prepareWorkspace({
sandbox,
sandboxId: 'usr-abcdef',
orgId: '123e4567-e89b-12d3-a456-426614174030',
userId: 'user_test',
sessionId: 'agent_test' as SessionId,
env: createEnv(),
metadata,
kilocodeModel: 'test-model',
});

expect(tokenMocks.issueCloudAgentBitbucketSessionCapability).toHaveBeenCalled();
expect(tokenMocks.resolveManagedBitbucketToken).not.toHaveBeenCalled();
// A kbb1. capability origin stays authenticated through the outbound
// interceptor, so it must NOT be stripped (unlike a raw-token session).
expect(workspaceMocks.updateGitRemoteUrl).not.toHaveBeenCalled();
});

it('writes the opaque Kilo capability to the sandbox auth file, never the raw token', async () => {
const session = createSession(false);
const writeFile = vi.fn().mockResolvedValue(undefined);
Expand Down Expand Up @@ -1144,6 +1182,41 @@ describe('SessionService.prepareWorkspace', () => {
);
});

it('preserves the capability origin (no strip, no refresh) for a contained warm Bitbucket review', async () => {
const session = createSession(true);
const sandbox = createSandbox(session, true);
const metadata = createBitbucketMetadata(true, '123e4567-e89b-12d3-a456-426614174030', {
github: false,
gitlab: false,
bitbucket: true,
kilocode: false,
});
tokenMocks.issueCloudAgentBitbucketSessionCapability.mockResolvedValue({
success: true,
value: {
capability: 'kbb1.opaque-capability',
gitUrl: 'https://bitbucket.org/acme-team/widgets.git',
},
});

await new SessionService().prepareWorkspace({
sandbox,
sandboxId: 'usr-abcdef',
orgId: '123e4567-e89b-12d3-a456-426614174030',
userId: 'user_test',
sessionId: 'agent_test' as SessionId,
env: createEnv(),
metadata,
kilocodeModel: 'test-model',
});

expect(workspaceMocks.cloneGitRepo).not.toHaveBeenCalled();
// A capability origin is preserved: no strip, and the warm-resume token
// refresh is skipped because sanitize reports the remote as handled.
expect(workspaceMocks.updateGitRemoteUrl).not.toHaveBeenCalled();
expect(workspaceMocks.updateGitRemoteToken).not.toHaveBeenCalled();
});

it('refreshes prepared GitHub workspace metadata with a managed capability', async () => {
const session = createSession(true);
const sandbox = createSandbox(session, true);
Expand Down
7 changes: 7 additions & 0 deletions services/cloud-agent-next/src/session-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2674,6 +2674,13 @@ export class SessionService {
if (metadata.identity.createdOnPlatform !== 'code-review' || git?.type !== 'bitbucket') {
return false;
}
// A contained session's origin carries a kbb1. capability that stays
// authenticated through the outbound interceptor, so it must stay in place for
// a blobless clone's later lazy blob fetches (mirrors the wrapper's
// sanitizeBitbucketCodeReviewRemote). Only a raw workspace token is stripped.
if (getEffectiveCredentialContainment(metadata).bitbucket) {
return true;
}
await updateGitRemoteUrl(session, workspacePath, git.url);
return true;
}
Expand Down