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
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ jobs:
# persona-kit is a leaf dep consumed by workload-router and cli, so it
# must publish first. The top-level `agentworkforce` wrapper depends on
# `@agentworkforce/cli`, so it must publish last.
echo "packages=persona-kit workload-router cli agentworkforce" >> "$GITHUB_OUTPUT"
echo "packages=persona-kit workload-router cli daytona-runner agentworkforce" >> "$GITHUB_OUTPUT"

# Lockstep baseline heal. The workspace publishes every package at the
# same version, so if any package's local version lags either its own
Expand Down Expand Up @@ -689,7 +689,7 @@ jobs:
// didn't publish an umbrella stamp (e.g. version: none re-runs).
const releaseVersion = process.env.RELEASE_VERSION || canonicalVersion;

const packageOrder = ['persona-kit', 'workload-router', 'cli', 'agentworkforce'];
const packageOrder = ['persona-kit', 'workload-router', 'cli', 'daytona-runner', 'agentworkforce'];
const entries = versionsRaw.trim().split(/\s+/).filter(Boolean).map((entry) => {
const idx = entry.indexOf(':');
return { pkg: entry.slice(0, idx), ver: entry.slice(idx + 1) };
Expand Down
2 changes: 2 additions & 0 deletions packages/daytona-runner/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist/
node_modules/
40 changes: 40 additions & 0 deletions packages/daytona-runner/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# @agentworkforce/daytona-runner

Daytona-backed `WorkflowRuntime` adapter for AgentWorkforce deploy workflows. This package owns the Daytona runtime implementation, auth helpers, and runtime contract types.

## Install

```sh
npm install @agentworkforce/daytona-runner @daytonaio/sdk
```

`@daytonaio/sdk` is a peer dependency — consumers bring their own version (^0.148.0).

## Usage

```ts
import { Daytona } from '@daytonaio/sdk';
import {
DaytonaRuntime,
resolveDaytonaAuthCredentials,
} from '@agentworkforce/daytona-runner';

const auth = resolveDaytonaAuthCredentials({
apiKey: process.env.DAYTONA_API_KEY,
jwtToken: process.env.DAYTONA_JWT_TOKEN,
organizationId: process.env.DAYTONA_ORGANIZATION_ID,
});

const daytona = new Daytona(auth);
const runtime = new DaytonaRuntime({ daytona });

const handle = await runtime.launch({ label: 'my-workflow' });
const result = await runtime.exec(handle, 'node -e "console.log(\\"ok\\")"');
await runtime.destroy(handle);
```

## Exports

- `DaytonaRuntime` — the `WorkflowRuntime` implementation.
- `resolveDaytonaAuthCredentials` / `applyDaytonaAuthEnv` — Daytona auth helpers (apiKey vs JWT+org).
- `WorkflowRuntime`, `RuntimeHandle`, `LaunchOptions`, `ExecOptions`, `ExecResult`, `RuntimeCapabilities` — runtime contract types.
42 changes: 42 additions & 0 deletions packages/daytona-runner/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "@agentworkforce/daytona-runner",
"version": "0.1.0",
"description": "Daytona-backed WorkflowRuntime adapter for AgentWorkforce deploy workflows.",
"private": false,
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"files": [
"dist",
"README.md",
"package.json"
],
"repository": {
"type": "git",
"url": "https://github.com/AgentWorkforce/workforce",
"directory": "packages/daytona-runner"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsc -p tsconfig.json",
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test": "tsc -p tsconfig.json && node --test dist/*.test.js",
"lint": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {},
"peerDependencies": {
"@daytonaio/sdk": "^0.148.0"
},
"devDependencies": {
"@daytonaio/sdk": "^0.148.0"
}
}
53 changes: 53 additions & 0 deletions packages/daytona-runner/src/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
export interface DaytonaAuthCredentials {
apiKey?: string;
jwtToken?: string;
organizationId?: string;
}

export type ResolvedDaytonaAuthCredentials =
| { apiKey: string }
| { jwtToken: string; organizationId: string };

export function resolveDaytonaAuthCredentials(
credentials: DaytonaAuthCredentials,
): ResolvedDaytonaAuthCredentials {
const apiKey = credentials.apiKey?.trim();
if (apiKey) {
return { apiKey };
}

const jwtToken = credentials.jwtToken?.trim();
if (!jwtToken) {
throw new Error('Daytona auth is required in credential bundle');
}

const organizationId = credentials.organizationId?.trim();
if (!organizationId) {
throw new Error('DAYTONA_ORGANIZATION_ID is required when using Daytona JWT auth');
}

return { jwtToken, organizationId };
}

export function applyDaytonaAuthEnv(
env: Record<string, string>,
credentials: DaytonaAuthCredentials,
): void {
const resolved = resolveDaytonaAuthCredentials(credentials);
if ('apiKey' in resolved) {
env.DAYTONA_API_KEY = resolved.apiKey;
// Clear the JWT-mode env keys so a caller flipping from JWT to apiKey
// auth doesn't leave stale credentials in the bag that downstream
// consumers might read and prefer over the apiKey path.
delete env.DAYTONA_JWT_TOKEN;
delete env.DAYTONA_ORGANIZATION_ID;
return;
}

env.DAYTONA_JWT_TOKEN = resolved.jwtToken;
env.DAYTONA_ORGANIZATION_ID = resolved.organizationId;
// Same logic in reverse: a JWT-auth caller should not see a lingering
// DAYTONA_API_KEY from an earlier mode that would silently take
// priority over the freshly-applied JWT.
delete env.DAYTONA_API_KEY;
}
21 changes: 21 additions & 0 deletions packages/daytona-runner/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export {
DaytonaRuntime,
type DaytonaAttachedSandboxOptions,
type DaytonaRuntimeOptions,
} from './runtime.js';

export {
applyDaytonaAuthEnv,
resolveDaytonaAuthCredentials,
type DaytonaAuthCredentials,
type ResolvedDaytonaAuthCredentials,
} from './auth.js';

export type {
ExecOptions,
ExecResult,
LaunchOptions,
RuntimeCapabilities,
RuntimeHandle,
WorkflowRuntime,
} from './types.js';
87 changes: 87 additions & 0 deletions packages/daytona-runner/src/runtime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { after, before, describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { Daytona } from '@daytonaio/sdk';

import * as pkg from './index.js';
import { DaytonaRuntime, applyDaytonaAuthEnv, resolveDaytonaAuthCredentials } from './index.js';
import type { RuntimeHandle } from './index.js';

describe('public barrel', () => {
it('exports DaytonaRuntime as a class', () => {
assert.equal(typeof pkg.DaytonaRuntime, 'function');
assert.equal(typeof DaytonaRuntime, 'function');
});

it('exports resolveDaytonaAuthCredentials and applyDaytonaAuthEnv', () => {
assert.equal(typeof pkg.resolveDaytonaAuthCredentials, 'function');
assert.equal(typeof resolveDaytonaAuthCredentials, 'function');
assert.equal(typeof pkg.applyDaytonaAuthEnv, 'function');
assert.equal(typeof applyDaytonaAuthEnv, 'function');
});

it('resolveDaytonaAuthCredentials normalises an apiKey-only input', () => {
const resolved = resolveDaytonaAuthCredentials({ apiKey: 'sk-test' });
assert.ok('apiKey' in resolved, 'expected apiKey-mode result');
assert.equal(resolved.apiKey, 'sk-test');
});

it('resolveDaytonaAuthCredentials rejects empty input', () => {
assert.throws(() => resolveDaytonaAuthCredentials({}));
});

it('applyDaytonaAuthEnv writes DAYTONA_API_KEY into the supplied env bag', () => {
const env: Record<string, string> = {};
applyDaytonaAuthEnv(env, { apiKey: 'sk-test' });
assert.equal(env.DAYTONA_API_KEY, 'sk-test');
});
});

const daytonaApiKey = process.env.DAYTONA_API_KEY?.trim();
const HAS_DAYTONA = Boolean(daytonaApiKey);
const SMOKE_LABEL = 'daytona-runner-smoke';

describe('DaytonaRuntime smoke', { concurrency: false }, () => {
let runtime: DaytonaRuntime | undefined;
let handle: RuntimeHandle | undefined;

before(() => {
if (!HAS_DAYTONA) return;
const auth = resolveDaytonaAuthCredentials({
apiKey: daytonaApiKey,
jwtToken: process.env.DAYTONA_JWT_TOKEN,
organizationId: process.env.DAYTONA_ORGANIZATION_ID,
});
const daytona = new Daytona(auth);
runtime = new DaytonaRuntime({ daytona });
});

after(async () => {
if (runtime && handle) {
try {
await runtime.destroy(handle);
} catch {
// best-effort cleanup; sandbox leaks surface via Daytona dashboard
}
}
});

it(
'launches a sandbox, runs node -e, and destroys it',
{ skip: HAS_DAYTONA ? false : 'DAYTONA_API_KEY is not set', timeout: 120_000 },
async () => {
assert.ok(runtime, 'runtime should be initialised when DAYTONA_API_KEY is set');
handle = await runtime.launch({ label: SMOKE_LABEL });
const result = await runtime.exec(handle, "node -e 'console.log(\"ok\")'");
assert.equal(
result.exitCode,
0,
`expected exitCode 0, got ${result.exitCode}: ${result.output}`,
);
assert.match(
result.output,
/\bok\b/,
`expected output to contain "ok", got: ${result.output}`,
);
},
);
});
Loading
Loading