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
38 changes: 38 additions & 0 deletions packages/deploy/src/connect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1892,6 +1892,44 @@ test('connectIntegrations treats a connected daytona integration as already-conn
assert.ok(io.messages.some((m) => m.level === 'info' && /daytona: already connected/.test(m.message)));
});

test('connectIntegrations ignores catalog config keys for CLI-captured daytona credentials', async () => {
const io = createBufferedIO();
let catalogLookupCalled = false;

const result = await connectIntegrations({
persona: {
id: 'daytona-monitor',
intent: 'relay-orchestrator',
description: 'test persona',
tags: ['discovery'],
integrations: { daytona: {} }
} as never,
workspace: 'ws-1',
noConnect: false,
io,
providerConfigKeys: {
async resolve(provider) {
catalogLookupCalled = true;
assert.equal(provider, 'daytona');
return 'daytona-relay';
}
},
integrations: {
async isConnected(args) {
assert.equal(args.provider, 'daytona');
assert.equal(args.expectedConfigKey, undefined);
return true;
},
async connect() {
throw new Error('should not connect when daytona is already connected');
}
}
});

assert.equal(catalogLookupCalled, false);
assert.deepEqual(result.outcomes, [{ provider: 'daytona', status: 'already-connected' }]);
});

test('connectIntegrations gates the deploy when daytona is not connected under --no-prompt', async () => {
const io = createBufferedIO();
let connectCalled = false;
Expand Down
21 changes: 18 additions & 3 deletions packages/deploy/src/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,20 @@ function isCliCapturedProvider(provider: string): boolean {
return CLI_CAPTURED_PROVIDERS.has(provider);
}

export async function resolveExpectedProviderConfigKey(
provider: string,
providerConfigKeys?: ProviderConfigKeyResolver
): Promise<string | undefined> {
if (!providerConfigKeys || isCliCapturedProvider(provider)) {
return undefined;
}
try {
return await providerConfigKeys.resolve(provider);
} catch {
return undefined;
}
}
Comment on lines +610 to +622

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If providerConfigKeys.resolve(provider) throws an error synchronously, the .catch() block will not be evaluated because the error is thrown before the promise is returned and the catch handler is attached. Since this is an async function, the synchronous throw will be caught by the async wrapper and result in a rejected promise, which will propagate to the caller and potentially crash the deployment process.\n\nUsing a standard try/catch block with await is safer and more idiomatic in TypeScript/JavaScript, as it gracefully handles both synchronous throws and asynchronous promise rejections.

export async function resolveExpectedProviderConfigKey(
  provider: string,
  providerConfigKeys?: ProviderConfigKeyResolver
): Promise<string | undefined> {
  if (!providerConfigKeys || isCliCapturedProvider(provider)) {
    return undefined;
  }
  try {
    return await providerConfigKeys.resolve(provider);
  } catch {
    return undefined;
  }
}


/**
* Walk the persona's declared integrations and ensure each is connected.
* Per the deploy-v1 spec, the orchestrator prompts before each provider's
Expand Down Expand Up @@ -638,9 +652,10 @@ export async function connectIntegrations(input: ConnectAllInput): Promise<Conne
const integrationEntry = integrations[provider] ?? {};
const source: IntegrationSource = integrationEntry.source ?? { kind: 'deployer_user' };
const forceReconnect = input.reconnectProviders?.includes(provider) ?? false;
const expectedConfigKey = input.providerConfigKeys
? await input.providerConfigKeys.resolve(provider).catch(() => undefined)
: undefined;
const expectedConfigKey = await resolveExpectedProviderConfigKey(
provider,
input.providerConfigKeys
);

let statusCheckFailure: string | undefined;
let connected = await checkProviderConnected(
Expand Down
105 changes: 105 additions & 0 deletions packages/deploy/src/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,111 @@ test('deploy dev mode runtime credential eligibility preserves expected provider
}
});

test('deploy dev mode ignores catalog config keys for CLI-captured daytona runtime credentials', async () => {
const { personaPath, cleanup } = await withTempPersona(
basePersonaJson({
integrations: {
daytona: {}
}
})
);
const io = createBufferedIO();
const originalFetch = globalThis.fetch;
const originalProviderToken = process.env.WORKFORCE_INTEGRATION_DAYTONA_TOKEN;
const urls: string[] = [];
let catalogLookupCount = 0;
let launchedEnv: Record<string, string> | undefined;

process.env.WORKFORCE_INTEGRATION_DAYTONA_TOKEN = 'WORKFORCE_DAYTONA_CONNECT_SENTINEL';
globalThis.fetch = (async (input, init) => {
const url = String(input);
urls.push(url);
if (url.includes('/api/v1/workspaces/ws-test/integrations/daytona/status')) {
return jsonResponse({
provider: 'daytona',
configKey: 'daytona',
backend: 'provider-credential',
ready: true,
connectionMatched: true,
oauth: { connected: true }
});
}
if (url.includes('/api/v1/workspaces/ws-test/runtime-credentials')) {
const body = init?.body ? JSON.parse(String(init.body)) : undefined;
assert.deepEqual(body, {
personaId: 'demo',
agentId: 'demo',
integrations: {
daytona: { source: { kind: 'deployer_user' } }
},
ttlSeconds: 3600
});
return jsonResponse({
relayfileUrl: 'https://relayfile.test',
relayfileWorkspaceId: 'ws-test',
relayfileToken: 'relay_pa_daytona',
relayfileMountPaths: ['/daytona/sandboxes/**']
});
}
throw new Error(`unexpected URL ${url}`);
}) as typeof fetch;

try {
await deploy(
{
personaPath,
mode: 'dev',
noPrompt: true,
cloudUrl: 'https://cloud.example.test',
io
},
{
workspaceAuth: {
async resolveWorkspace() {
return { workspace: 'ws-test', token: 'relay_ws_workspace' };
}
},
providerConfigKeys: {
async resolve(provider) {
catalogLookupCount += 1;
assert.equal(provider, 'daytona');
return 'daytona-relay';
}
},
bundle: successfulBundleStager(),
modes: {
dev: {
async launch(input) {
launchedEnv = input.env;
return {
id: 'dev-1',
async stop() {
/* no-op */
},
done: Promise.resolve({ code: 0 })
};
}
}
}
}
);

assert.equal(catalogLookupCount, 0);
assert.equal(launchedEnv?.RELAYFILE_TOKEN, 'relay_pa_daytona');
assert.equal(launchedEnv?.WORKFORCE_INTEGRATION_DAYTONA_TOKEN, '');
assert.ok(urls.find((url) => url.includes('/integrations/daytona/status')));
assert.ok(urls.find((url) => url.endsWith('/runtime-credentials')));
} finally {
if (originalProviderToken === undefined) {
delete process.env.WORKFORCE_INTEGRATION_DAYTONA_TOKEN;
} else {
process.env.WORKFORCE_INTEGRATION_DAYTONA_TOKEN = originalProviderToken;
}
globalThis.fetch = originalFetch;
await cleanup();
}
});

test('deploy dev mode rejects malformed runtime credential tokens before launch', async () => {
const { personaPath, cleanup } = await withTempPersona(
basePersonaJson({
Expand Down
8 changes: 5 additions & 3 deletions packages/deploy/src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
relayfileCatalogConfigKeyResolver,
relayfileIntegrationResolver,
relayfileOptionsResolver,
resolveExpectedProviderConfigKey,
type ConnectAllInput,
type IntegrationAuthRecoveryResolver,
type IntegrationConnectResolver,
Expand Down Expand Up @@ -449,9 +450,10 @@ async function resolveRuntimeCredentialEnv(args: {
workspaceToken: args.workspaceToken
});
for (const [provider, integration] of Object.entries(integrations)) {
const expectedConfigKey = args.providerConfigKeys
? await args.providerConfigKeys.resolve(provider).catch(() => undefined)
: undefined;
const expectedConfigKey = await resolveExpectedProviderConfigKey(
provider,
args.providerConfigKeys
);
const connected = await relayfile
.isConnected({
workspace: args.workspace,
Expand Down
Loading