Skip to content

Commit 683dff7

Browse files
thrashr888claude
andcommitted
feat: add Swift SDK and SDK publishing workflow
Swift SDK (sdk/swift/): actor-based client with async/await, zero third-party deps, URLProtocol-based mock tests (18 pass), SSE streaming, withSandbox auto-cleanup closure, Sendable types. sdk-publish.yml: 4 parallel publish jobs on tag push — npm + GitHub Packages (dual-publish), PyPI via OIDC, crates.io, Swift verify-only (SPM uses Git tags). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 23bdf79 commit 683dff7

13 files changed

Lines changed: 1253 additions & 0 deletions

File tree

.github/workflows/sdk-publish.yml

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
name: SDK Publish
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
8+
permissions:
9+
contents: read
10+
packages: write
11+
12+
jobs:
13+
# ── Node.js: npm + GitHub Packages ──────────────────────────────────
14+
publish-nodejs:
15+
name: Publish Node.js SDK
16+
runs-on: ubuntu-latest
17+
defaults:
18+
run:
19+
working-directory: sdk/nodejs
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- uses: actions/setup-node@v4
24+
with:
25+
node-version: 20
26+
registry-url: https://registry.npmjs.org
27+
28+
- run: npm ci
29+
- run: npm run build
30+
- run: npm test
31+
32+
# Extract version from tag
33+
- name: Set version from tag
34+
env:
35+
TAG_NAME: ${{ github.ref_name }}
36+
run: npm version "${TAG_NAME#v}" --no-git-tag-version
37+
38+
# Publish to npm
39+
- run: npm publish --access public
40+
env:
41+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
42+
43+
# Publish to GitHub Packages
44+
- uses: actions/setup-node@v4
45+
with:
46+
node-version: 20
47+
registry-url: https://npm.pkg.github.com
48+
49+
- name: Publish to GitHub Packages
50+
env:
51+
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
52+
run: |
53+
# GitHub Packages requires scoped name
54+
node -e "
55+
const pkg = require('./package.json');
56+
pkg.name = '@thrashr888/agentkernel';
57+
require('fs').writeFileSync('package.json', JSON.stringify(pkg, null, 2));
58+
"
59+
npm publish --access public
60+
git checkout package.json
61+
62+
# ── Python: PyPI via OIDC trusted publisher ─────────────────────────
63+
publish-python:
64+
name: Publish Python SDK
65+
runs-on: ubuntu-latest
66+
environment: pypi
67+
permissions:
68+
id-token: write
69+
defaults:
70+
run:
71+
working-directory: sdk/python
72+
steps:
73+
- uses: actions/checkout@v4
74+
75+
- uses: actions/setup-python@v5
76+
with:
77+
python-version: "3.12"
78+
79+
- name: Install build tools
80+
run: pip install build
81+
82+
- name: Set version from tag
83+
env:
84+
TAG_NAME: ${{ github.ref_name }}
85+
run: |
86+
VERSION="${TAG_NAME#v}"
87+
sed -i "s/^version = .*/version = \"$VERSION\"/" pyproject.toml
88+
89+
- name: Build package
90+
run: python -m build
91+
92+
- name: Publish to PyPI
93+
uses: pypa/gh-action-pypi-publish@release/v1
94+
with:
95+
packages-dir: sdk/python/dist/
96+
97+
# ── Rust: crates.io ─────────────────────────────────────────────────
98+
publish-rust:
99+
name: Publish Rust SDK
100+
runs-on: ubuntu-latest
101+
defaults:
102+
run:
103+
working-directory: sdk/rust
104+
steps:
105+
- uses: actions/checkout@v4
106+
107+
- uses: dtolnay/rust-toolchain@stable
108+
109+
- name: Set version from tag
110+
env:
111+
TAG_NAME: ${{ github.ref_name }}
112+
run: |
113+
VERSION="${TAG_NAME#v}"
114+
sed -i "s/^version = .*/version = \"$VERSION\"/" Cargo.toml
115+
116+
- run: cargo test
117+
118+
- name: Publish to crates.io
119+
env:
120+
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
121+
run: cargo publish --token "$CARGO_REGISTRY_TOKEN"
122+
123+
# ── Swift: verify only (SPM uses Git tags) ──────────────────────────
124+
verify-swift:
125+
name: Verify Swift SDK
126+
runs-on: macos-latest
127+
defaults:
128+
run:
129+
working-directory: sdk/swift
130+
steps:
131+
- uses: actions/checkout@v4
132+
133+
- name: Build
134+
run: swift build
135+
136+
- name: Test
137+
run: swift test

sdk/swift/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.build/
2+
.swiftpm/
3+
Package.resolved
4+
*.xcodeproj
5+
xcuserdata/
6+
DerivedData/
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Quickstart example for the AgentKernel Swift SDK.
2+
//
3+
// Usage: swift run quickstart
4+
// Requires: agentkernel server running on localhost:8080
5+
6+
import AgentKernel
7+
8+
@main
9+
struct Quickstart {
10+
static func main() async throws {
11+
let client = AgentKernel()
12+
13+
// Health check
14+
let status = try await client.health()
15+
print("Server status: \(status)")
16+
17+
// Run a command
18+
let output = try await client.run(["echo", "Hello from Swift!"])
19+
print("Output: \(output.output)")
20+
21+
// List sandboxes
22+
let sandboxes = try await client.listSandboxes()
23+
print("Active sandboxes: \(sandboxes.count)")
24+
for sb in sandboxes {
25+
print(" - \(sb.name) (\(sb.status), \(sb.backend))")
26+
}
27+
}
28+
}

sdk/swift/Examples/sandbox.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Sandbox session example for the AgentKernel Swift SDK.
2+
//
3+
// Demonstrates withSandbox for guaranteed cleanup.
4+
// Requires: agentkernel server running on localhost:8080
5+
6+
import AgentKernel
7+
8+
@main
9+
struct SandboxExample {
10+
static func main() async throws {
11+
let client = AgentKernel()
12+
13+
// withSandbox guarantees cleanup even if the closure throws.
14+
let result: String = try await client.withSandbox("swift-demo", image: "python:3.12-alpine") { session in
15+
print("Sandbox '\(session.name)' created")
16+
17+
// Run commands inside the sandbox
18+
let hello = try await session.run(["echo", "Hello from sandbox!"])
19+
print(" \(hello.output)")
20+
21+
let pyVersion = try await session.run(["python", "--version"])
22+
print(" Python: \(pyVersion.output)")
23+
24+
// Get sandbox info
25+
let info = try await session.info()
26+
print(" Status: \(info.status), Backend: \(info.backend)")
27+
28+
return pyVersion.output
29+
}
30+
// Sandbox is now removed automatically
31+
print("Done. Python version was: \(result)")
32+
}
33+
}

sdk/swift/Package.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// swift-tools-version: 5.9
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "AgentKernel",
7+
platforms: [
8+
.macOS(.v13),
9+
.iOS(.v16),
10+
],
11+
products: [
12+
.library(name: "AgentKernel", targets: ["AgentKernel"]),
13+
],
14+
targets: [
15+
.target(
16+
name: "AgentKernel",
17+
path: "Sources/AgentKernel"
18+
),
19+
.testTarget(
20+
name: "AgentKernelTests",
21+
dependencies: ["AgentKernel"],
22+
path: "Tests/AgentKernelTests"
23+
),
24+
]
25+
)

sdk/swift/README.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# agentkernel Swift SDK
2+
3+
Swift client for the agentkernel API. Zero third-party dependencies.
4+
5+
## Install
6+
7+
Add to your `Package.swift`:
8+
9+
```swift
10+
dependencies: [
11+
.package(url: "https://github.com/thrashr888/agentkernel.git", from: "0.1.0")
12+
]
13+
```
14+
15+
Then add the dependency to your target:
16+
17+
```swift
18+
.target(name: "MyApp", dependencies: [
19+
.product(name: "AgentKernel", package: "agentkernel")
20+
])
21+
```
22+
23+
## Quick Start
24+
25+
```swift
26+
import AgentKernel
27+
28+
let client = AgentKernel()
29+
30+
let output = try await client.run(["echo", "hello"])
31+
print(output.output)
32+
```
33+
34+
## Configuration
35+
36+
```swift
37+
// Explicit options
38+
let client = AgentKernel(AgentKernelOptions(
39+
baseURL: "http://localhost:8080",
40+
apiKey: "sk-...",
41+
timeout: 30
42+
))
43+
44+
// Or use environment variables:
45+
// AGENTKERNEL_BASE_URL, AGENTKERNEL_API_KEY
46+
let client = AgentKernel()
47+
```
48+
49+
## Usage
50+
51+
### Run a Command
52+
53+
```swift
54+
let output = try await client.run(["python", "-c", "print('hi')"])
55+
print(output.output)
56+
```
57+
58+
With options:
59+
60+
```swift
61+
let opts = RunOptions(image: "python:3.12-alpine", profile: .restrictive, fast: false)
62+
let output = try await client.run(["python", "-c", "print('hi')"], options: opts)
63+
```
64+
65+
### Stream Output (SSE)
66+
67+
```swift
68+
let stream = try await client.runStream(["long-running-task"])
69+
for try await event in stream {
70+
print("[\(event.eventType)] \(event.data)")
71+
}
72+
```
73+
74+
### Sandbox Lifecycle
75+
76+
```swift
77+
// Create
78+
let sb = try await client.createSandbox("my-sandbox")
79+
80+
// Execute
81+
let output = try await client.execInSandbox("my-sandbox", command: ["ls", "-la"])
82+
83+
// Info
84+
let info = try await client.getSandbox("my-sandbox")
85+
86+
// List all
87+
let all = try await client.listSandboxes()
88+
89+
// Remove
90+
try await client.removeSandbox("my-sandbox")
91+
```
92+
93+
### Scoped Sandbox (Recommended)
94+
95+
`withSandbox` guarantees cleanup even if the closure throws:
96+
97+
```swift
98+
let result = try await client.withSandbox("temp", image: "node:20-alpine") { session in
99+
let output = try await session.run(["node", "-e", "console.log('hi')"])
100+
return output.output
101+
}
102+
```
103+
104+
## Error Handling
105+
106+
```swift
107+
do {
108+
let output = try await client.run(["bad-command"])
109+
} catch let error as AgentKernelError {
110+
switch error {
111+
case .auth(let msg): print("Auth: \(msg)")
112+
case .validation(let msg): print("Validation: \(msg)")
113+
case .notFound(let msg): print("Not found: \(msg)")
114+
case .server(let msg): print("Server: \(msg)")
115+
case .network(let err): print("Network: \(err)")
116+
case .stream(let msg): print("Stream: \(msg)")
117+
case .json(let err): print("JSON: \(err)")
118+
}
119+
}
120+
```
121+
122+
## Requirements
123+
124+
- Swift 5.9+
125+
- macOS 13+ / iOS 16+ / Linux
126+
127+
## License
128+
129+
MIT

0 commit comments

Comments
 (0)