Skip to content
Open
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
2 changes: 1 addition & 1 deletion provider-swift/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ cp /tmp/mlxvenv/lib/python*/site-packages/mlx/lib/mlx.metallib \
.build/release/mlx.metallib

# Then:
.build/release/darkbloom serve --foreground
.build/release/darkbloom serve --model mlx-community/Qwen3-4B-4bit
```

`release-swift.yml` in CI does the same thing automatically and bakes the metallib into the released bundle next to `darkbloom`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,17 @@ public struct SignedAttestation: Codable, Sendable {
public let signature: String // base64 DER-encoded ECDSA signature
}

public enum AttestationInputValidator {
public static let x25519PublicKeyByteCount = 32

public static func isValidX25519PublicKeyBase64(_ value: String) -> Bool {
guard let data = Data(base64Encoded: value, options: [.ignoreUnknownCharacters]) else {
return false
}
return data.count == x25519PublicKeyByteCount
}
}

/// Fields covered by `status_signature` in an attestation challenge response.
public struct StatusCanonicalInput: Sendable, Equatable {
public var nonce: String
Expand Down
21 changes: 15 additions & 6 deletions provider-swift/Sources/darkbloom-enclave-cli/EnclaveCLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// attest --pub-key <b64> Build a signed attestation blob and print JSON.
// sign --message <s> Sign a message with the SE key (base64 DER sig).
// info Print public key info (base64 + hex).
// wallet-address Print the deterministic identifier derived from the SE key.
// wallet-address Print an ephemeral identifier derived from the SE key.
//
// All operations create a fresh, ephemeral Secure Enclave key pair. The
// tool is stateless: there is no on-disk key material.
Expand All @@ -34,11 +34,14 @@ struct DarkbloomEnclave: ParsableCommand {

private enum EnclaveCLIError: Error, CustomStringConvertible {
case secureEnclaveUnavailable
case invalidPublicKey(String)

var description: String {
switch self {
case .secureEnclaveUnavailable:
return "Secure Enclave is unavailable on this device (Intel Mac or non-Apple hardware?)"
case .invalidPublicKey(let value):
return "--pub-key must be a base64-encoded 32-byte X25519 public key (got '\(value)')"
}
}
}
Expand All @@ -64,6 +67,10 @@ struct Attest: ParsableCommand {
var binaryHash: String?

func run() throws {
if let pubKey, !AttestationInputValidator.isValidX25519PublicKeyBase64(pubKey) {
throw EnclaveCLIError.invalidPublicKey(pubKey)
}

let identity = try loadIdentity()
let builder = AttestationBuilder(identity: identity)
let data = try builder.buildAttestationJSON(
Expand Down Expand Up @@ -119,14 +126,16 @@ struct Info: ParsableCommand {

struct WalletAddress: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Print a stable identifier (0x-prefixed 20-byte hex) derived from the SE public key."
abstract: "Print an ephemeral identifier (0x-prefixed 20-byte hex) derived from a fresh SE public key.",
discussion: "The helper is stateless and creates a new Secure Enclave key for each invocation; this value is not durable identity."
)

func run() throws {
// The SE produces P-256 keys, not secp256k1; we expose a stable
// identifier derived from the SE pubkey rather than an Ethereum
// address. Coordinators that previously used Ethereum-style hex
// wallets accept any 20-byte hex prefixed with "0x".
// The SE produces P-256 keys, not secp256k1; this stateless helper
// creates a fresh key on every invocation, so the printed value is
// only an ephemeral diagnostic identifier. Coordinators that previously
// used Ethereum-style hex wallets accept any 20-byte hex prefixed with
// "0x".
let identity = try loadIdentity()
let pubKeyHash = SHA256.hash(data: identity.publicKey.rawRepresentation)
let last20 = Array(pubKeyHash).suffix(20)
Expand Down
3 changes: 2 additions & 1 deletion provider-swift/Sources/darkbloom/Darkbloom.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import Foundation
import ArgumentParser
import ProviderCore

@main
@available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *)
struct Darkbloom: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "darkbloom",
abstract: "Swift-native provider CLI for Darkbloom.",
discussion: "Runs on Apple Silicon Macs. Connects to the coordinator, serves inference requests via mlx-swift.",
version: ProviderCore.version,
subcommands: [
Serve.self,
Start.self,
Stop.self,
Status.self,
Expand Down
51 changes: 51 additions & 0 deletions provider-swift/Sources/darkbloom/ServeCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import Foundation
import ArgumentParser
import ProviderCore

@available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *)
struct Serve: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Run the provider in the foreground.",
discussion: """
Foreground provider mode for operators and local debugging. This connects
to the coordinator and keeps running until interrupted. Use `darkbloom start`
for the launchd-managed background service.
"""
)

@OptionGroup var configOptions: ConfigOptions

@Option(help: "Override coordinator WebSocket URL.")
var coordinatorURL: String?

@Option(help: "Model ID to serve (repeatable).")
var model: [String] = []

@Flag(help: "Serve all local models.")
var all = false

@Option(help: "Idle timeout in minutes before unloading the model.")
var idleTimeout: UInt64?

mutating func run() async throws {
var args: [String] = ["--foreground"]
if let config = configOptions.config {
args += ["--config", config]
}
if let coordinatorURL {
args += ["--coordinator-url", coordinatorURL]
}
for modelID in model {
args += ["--model", modelID]
}
if all {
args.append("--all")
}
if let idleTimeout {
args += ["--idle-timeout", "\(idleTimeout)"]
}

var start = try Start.parse(args)
try await start.run()
}
}
27 changes: 27 additions & 0 deletions provider-swift/Sources/darkbloom/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import ArgumentParser
import Foundation

let arguments = Array(CommandLine.arguments.dropFirst())
let knownSubcommands = Set(Darkbloom.configuration.subcommands.flatMap { command -> [String] in
[command._commandName] + command.configuration.aliases
})

if let first = arguments.first,
!first.hasPrefix("-"),
first != "help",
!knownSubcommands.contains(first)
{
FileHandle.standardError.write(Data("Error: Unknown subcommand \"\(first)\"\n".utf8))
Foundation.exit(64)
}

do {
var command = try Darkbloom.parseAsRoot(arguments)
if var asyncCommand = command as? AsyncParsableCommand {
try await asyncCommand.run()
} else {
try command.run()
}
} catch {
Darkbloom.exit(withError: error)
}
10 changes: 10 additions & 0 deletions provider-swift/Tests/ProviderCoreTests/SecurityTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,16 @@ import Testing
#expect(report.coordinatorRuntimeHashes.templateHashes == report.templateHashes)
}

@Test func attestationInputValidatorRequiresBase64X25519PublicKey() throws {
let valid = Data(repeating: 0x42, count: AttestationInputValidator.x25519PublicKeyByteCount)
.base64EncodedString()

#expect(AttestationInputValidator.isValidX25519PublicKeyBase64(valid))
#expect(!AttestationInputValidator.isValidX25519PublicKeyBase64("not-base64"))
#expect(!AttestationInputValidator.isValidX25519PublicKeyBase64(Data(repeating: 0x42, count: 31).base64EncodedString()))
#expect(!AttestationInputValidator.isValidX25519PublicKeyBase64(Data(repeating: 0x42, count: 33).base64EncodedString()))
}

@Test func statusCanonicalMatchesCoordinatorGoldenBytes() throws {
let data = try StatusCanonical.build(StatusCanonicalInput(
nonce: "test-nonce",
Expand Down