Hand-written Kotlin SDK for the Arcane API, for Android (and any JVM) apps that talk to an Arcane manager or agent.
libarcane-kotlin is a single-layer, idiomatic Kotlin client built on Ktor and kotlinx.serialization. There is no code generation: every DTO and every endpoint method is hand-crafted to match the Arcane API's types and HTTP surface.
Two Gradle modules:
arcane-core— pure Kotlin/JVM. Auth, token storage interface, environment scoping, REST helpers, WebSocket + NDJSON streams (asFlows), and per-resource services. Runs on any JVM and is unit-tested with Ktor'sMockEngine(no device/emulator).arcane-android— thin Android layer: a Keystore-backed secureTokenStore, OIDC Custom Tabs, and Arcane's same-origin passkey browser bridge backed by Android's credential provider. Apps using only API-key or username/password auth, or providing their own token storage, can depend onarcane-corealone.
Concurrency is coroutines-first: blocking calls are suspend functions and streams are Flows.
// settings.gradle.kts of a consuming project (once published):
dependencies {
implementation("app.getarcane:arcane-core:<version>") // JVM/Android core
implementation("app.getarcane:arcane-android:<version>") // Android secure storage + OIDC (optional)
}arcane-core: Kotlin/JVM (JVM 17 bytecode). arcane-android: com.android.library, minSdk 24.
import app.getarcane.sdk.ArcaneClient
import app.getarcane.sdk.ArcaneConfiguration
import app.getarcane.sdk.EnvironmentId
import app.getarcane.sdk.errors.ArcaneError
val client = ArcaneClient(
ArcaneConfiguration(
baseUrl = "https://arcane.example.com",
// On Android, use AndroidSecureTokenStore(context) from arcane-android.
defaultEnvironmentId = EnvironmentId("0"),
),
)
// The client owns an HttpClient + coroutine scope — close it when done (or use `client.use { }`).
try {
client.auth.login(username = "admin", password = "password")
val containers = client.containers.list(envId = EnvironmentId("0"))
val first = containers.data.first()
client.containers.start(envId = EnvironmentId("0"), id = first.id)
// Stream logs as a Flow.
client.containers.logs(envId = EnvironmentId("0"), id = first.id, follow = true)
.collect { line -> println(line.text) }
} catch (e: ArcaneError.Unauthorized) {
// ...
} catch (e: ArcaneError.Validation) {
e.fields.forEach { (field, messages) -> println("$field: $messages") }
} finally {
client.close()
}Three paths:
- API key — set
apiKeyonArcaneConfiguration; sent asX-API-Key(takes precedence over a bearer token). - Username / password —
client.auth.login(username, password). Tokens are cached and persisted via the configuredTokenStore; a 401 triggers a singleauth/refresh(concurrent calls are de-duplicated) and one retry. - OIDC — on Android,
OidcAuthenticator(client)drives the Custom Tabs flow (startSignIn→ app redirect →completeSignIn), or the device-code flow (beginDeviceFlow/pollDeviceToken). - Passkeys / MFA —
client.passkeysowns the typed begin/finish, step-up, enrollment, recovery, and mobile-login contracts. On Android,AndroidPasskeyBrowserBridge(client)preserves Arcane's required server origin while the browser invokes the platform credential provider. Ceremony JSON stays opaque and is never included in SDK diagnostics.
import app.getarcane.sdk.android.AndroidSecureTokenStore
val client = ArcaneClient(
ArcaneConfiguration(
baseUrl = "https://arcane.example.com",
tokenStore = AndroidSecureTokenStore(context), // AES-256-GCM via AndroidKeyStore + DataStore
),
)InMemoryTokenStore (in arcane-core) is the default and is used in tests.
client.containers.logs(...)/swarm.serviceLogs(...)/projects.logs(...)→Flow<LogLine>(WebSocket)client.containers.stats(...)/system.statsStream(...)→Flow<...>(WebSocket)client.containers.exec(...)→TerminalSession(bidirectional:send(text)+output: Flow<ByteArray>)client.images.pullStream(...)/projects.deployStream(...)→Flow<...>(NDJSON progress)client.dashboard.stream(...)→Flow<DashboardStreamEvent>(aggregated multi-environment snapshots)
Collecting a stream opens the connection; cancelling the collector closes it.
All failures surface as the sealed ArcaneError: Unauthorized, Forbidden, NotFound, Conflict, Validation(fields), RateLimited(retryAfter), Server(code, message), Transport, Decoding, Unknown.
After the first authenticated User is decoded, client.serverCapabilities() reports whether the server speaks v1 legacy roles or v2 RBAC, so apps can gate role-management UI.
Each resource is exposed as a service on ArcaneClient:
| Service | Endpoints |
|---|---|
client.auth |
login, logout, refresh, current-account update/avatar, password change, OIDC flow |
client.passkeys |
passkey login/enrollment, MFA, step-up, recovery, mobile exchange |
client.users |
user CRUD, avatars, role assignments |
client.apiKeys |
API key CRUD |
client.roles / client.oidcRoleMappings |
v2 RBAC roles + OIDC mappings |
client.environments |
environment CRUD, agent pairing, mTLS bundle |
client.containers |
list, inspect, lifecycle, logs, stats, exec |
client.images |
list, inspect, attestations, pull, build, prune, upload |
client.volumes |
volumes, browse, backups |
client.networks |
list, inspect, create, prune, topology |
client.projects |
compose projects: file-tree editing, up/down/restart/redeploy/build/pull/destroy/archive |
client.swarm |
swarm: nodes, services, stacks, configs, secrets, tasks |
client.system |
docker info, prune, convert, single/fleet upgrade, bulk actions |
client.dashboard |
env overview, action items, aggregated snapshot stream |
client.events |
audit events |
client.webhooks / client.notifications |
webhook + notification config |
client.templates / client.registries |
templates + container registries |
client.variables |
scoped global-variable CRUD and environment sync |
client.gitops / client.builds / client.jobs |
GitOps, build workspaces, scheduled jobs |
client.settings / client.updater / client.vulnerabilities / client.ports / client.version |
misc |
Binary downloads return ByteArray by default. Large build-workspace files and volume files/backups
also have Path overloads that stream to a caller-selected destination and replace it only after a
successful download.
./gradlew :arcane-core:test # JVM unit tests (no device needed)
./gradlew :arcane-android:assembleRelease
ARCANE_TEST_URL=https://your-arcane ./gradlew :arcane-core:test # also runs the live /health integration test