Shared Configuration
This guide explains how other native apps can discover and connect to the locally running Osaurus server, using a small JSON file Osaurus publishes to a well-known location.
- Audience: Developers of macOS apps (Swift/Objective‑C/SwiftUI/Electron) that want to integrate with Osaurus.
- License: Osaurus is fully open source. You are welcome to use this mechanism freely.
What gets published
Osaurus writes a per‑process shared configuration file so other processes can discover the server address and status.
- Base directory:
~/Library/Application Support/com.dinoki.osaurus/SharedConfiguration/ - Per‑instance directory:
<Base>/<instanceId>/ - File:
configuration.json
Osaurus may have multiple instances (e.g., after crashes or parallel runs). Each running instance gets its own instanceId directory. Instances that stop will remove their directory.
JSON schema
When the server is starting (minimal metadata):
{
"instanceId": "f26f8b59-2b64-4c57-8c5a-5a1ce9f9b4a8",
"updatedAt": "2025-09-08T12:34:56Z",
"health": "starting"
}
When the server is running:
{
"instanceId": "f26f8b59-2b64-4c57-8c5a-5a1ce9f9b4a8",
"updatedAt": "2025-09-08T12:35:12Z",
"port": 1337,
"address": "127.0.0.1",
"url": "http://127.0.0.1:1337",
"exposeToNetwork": false,
"health": "running"
}
instanceId(string): Unique per Osaurus app run.updatedAt(ISO‑8601 string): Last time Osaurus refreshed the file.health(string): One ofstartingorrunning.port(int): HTTP port whenhealth == "running".address(string): Bind address (e.g.,127.0.0.1or LAN IP) when running.url(string): Convenience URL when running.exposeToNetwork(bool): If true, server is reachable on the LAN; if false it is only on localhost.
When the server is stopping, stopped, or errored, Osaurus removes the instance directory/file.
Discovery strategy (recommended)
- Look in
~/Library/Application Support/com.dinoki.osaurus/SharedConfiguration/. - Enumerate all
<instanceId>subdirectories. - For each, read
configuration.jsonif it exists. - Filter to entries with
health == "running". - If multiple are running, pick the one with the most recent
updatedAt(fallback: directory mtime).
This approach gracefully handles multiple instances and transient startup states.
Swift sample: Discover and read Osaurus
You can copy/paste this into your macOS app. It finds the most recent running Osaurus instance and returns its URL.
import Foundation
struct OsaurusSharedConfiguration: Decodable {
let instanceId: String
let updatedAt: String
let health: String
let port: Int?
let address: String?
let url: String?
let exposeToNetwork: Bool?
}
struct OsaurusInstance {
let instanceId: String
let updatedAt: Date
let address: String
let port: Int
let url: URL
let exposeToNetwork: Bool
}
enum OsaurusDiscoveryError: Error {
case notFound
}
final class OsaurusDiscoveryService {
// Canonical base path used by Osaurus
private static let bundleIdentifier = "com.dinoki.osaurus"
static func discoverLatestRunningInstance() throws -> OsaurusInstance {
let fm = FileManager.default
let supportDir = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
let base = supportDir
.appendingPathComponent(bundleIdentifier, isDirectory: true)
.appendingPathComponent("SharedConfiguration", isDirectory: true)
guard let instanceDirs = try? fm.contentsOfDirectory(at: base, includingPropertiesForKeys: [.contentModificationDateKey, .isDirectoryKey], options: [.skipsHiddenFiles]), !instanceDirs.isEmpty else {
throw OsaurusDiscoveryError.notFound
}
var candidates: [OsaurusInstance] = []
for dir in instanceDirs {
var isDirectory: ObjCBool = false
guard fm.fileExists(atPath: dir.path, isDirectory: &isDirectory), isDirectory.boolValue else { continue }
let fileURL = dir.appendingPathComponent("configuration.json")
guard fm.fileExists(atPath: fileURL.path) else { continue }
do {
let data = try Data(contentsOf: fileURL)
let cfg = try JSONDecoder().decode(OsaurusSharedConfiguration.self, from: data)
guard cfg.health == "running", let address = cfg.address, let port = cfg.port else { continue }
// Parse updatedAt (fallback to dir mtime)
let parsedDate: Date
if let d = ISO8601DateFormatter().date(from: cfg.updatedAt) {
parsedDate = d
} else {
let attr = try fm.attributesOfItem(atPath: dir.path)
parsedDate = attr[.modificationDate] as? Date ?? Date.distantPast
}
let urlString = cfg.url ?? "http://\(address):\(port)"
guard let finalURL = URL(string: urlString) else { continue }
candidates.append(OsaurusInstance(
instanceId: cfg.instanceId,
updatedAt: parsedDate,
address: address,
port: port,
url: finalURL,
exposeToNetwork: cfg.exposeToNetwork ?? false
))
} catch {
// ignore malformed entries
}
}
guard let latest = candidates.sorted(by: { $0.updatedAt > $1.updatedAt }).first else {
throw OsaurusDiscoveryError.notFound
}
return latest
}
}
Electron/Node sample
Node.js helper to discover Osaurus:
// main/osaurus-discovery.js
const fs = require("fs/promises");
const os = require("os");
const path = require("path");
async function discoverLatestRunningInstance() {
const base = path.join(
os.homedir(),
"Library",
"Application Support",
"com.dinoki.osaurus",
"SharedConfiguration"
);
let entries;
try {
entries = await fs.readdir(base, { withFileTypes: true });
} catch {
throw new Error("Osaurus not found");
}
const candidates = [];
for (const dirent of entries) {
if (!dirent.isDirectory()) continue;
const dirPath = path.join(base, dirent.name);
const filePath = path.join(dirPath, "configuration.json");
try {
const data = await fs.readFile(filePath, "utf8");
const cfg = JSON.parse(data);
if (cfg.health !== "running" || !cfg.port || !cfg.address) continue;
let updatedAt = Date.parse(cfg.updatedAt);
if (Number.isNaN(updatedAt)) {
// Fallback to dir mtime
const stat = await fs.stat(dirPath);
updatedAt = stat.mtimeMs;
}
const url = cfg.url || `http://${cfg.address}:${cfg.port}`;
candidates.push({
instanceId: cfg.instanceId,
updatedAt,
address: cfg.address,
port: cfg.port,
url,
exposeToNetwork: !!cfg.exposeToNetwork,
});
} catch (_) {
// ignore malformed entries
}
}
if (candidates.length === 0) {
throw new Error("Osaurus not found");
}
candidates.sort((a, b) => b.updatedAt - a.updatedAt);
return candidates[0];
}
module.exports = { discoverLatestRunningInstance };
Usage from Electron main process:
// main/index.js
const { app, BrowserWindow, ipcMain } = require("electron");
const path = require("path");
const { discoverLatestRunningInstance } = require("./osaurus-discovery");
ipcMain.handle("osaurus:getInstance", async () => {
try {
return await discoverLatestRunningInstance();
} catch (e) {
return null;
}
});
async function createWindow() {
const win = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
},
});
await win.loadURL("file://" + path.join(__dirname, "index.html"));
}
app.whenReady().then(createWindow);
Preload bridge (renderer-safe access via IPC):
// main/preload.js
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("osaurus", {
getInstance: () => ipcRenderer.invoke("osaurus:getInstance"),
});
Renderer usage:
// renderer/index.js
async function connectToOsaurus() {
const inst = await window.osaurus.getInstance();
if (!inst) {
console.log("Osaurus not running");
return;
}
console.log("Osaurus at", inst.url, "LAN:", !!inst.exposeToNetwork);
// Example request (Node 18+ has global fetch in Electron; otherwise use axios/node-fetch)
const resp = await fetch(new URL("/v1/models", inst.url));
const models = await resp.json();
console.log(models);
}
connectToOsaurus();
Notes:
- The paths assume macOS; Electron must run on macOS to read
~/Library/Application Support/.... - Use the main process for filesystem access; avoid direct fs from the renderer.
- If you need all instances, return the full
candidateslist instead of the newest one.
Security and sandboxing
- Non‑sandboxed macOS apps can read
~/Library/Application Support/com.dinoki.osaurus/...directly. - Sandboxed apps typically cannot read arbitrary paths. Options:
- Ask the user to choose the
SharedConfigurationfolder withNSOpenPaneland persist a security‑scoped bookmark. - Or run a small non‑sandboxed helper that performs discovery and hands you the URL via XPC.
- Ask the user to choose the
Osaurus does not write secrets into the shared file; it only publishes connection details and status.
Troubleshooting
- If you see only
health: starting, wait briefly and retry. - If there are no instance folders, Osaurus is not running.
- If multiple instances exist, prefer the most recent
updatedAt. - When the user quits Osaurus, the instance directory is removed.
Stable identifiers
- Bundle identifier:
com.dinoki.osaurus - Base path:
~/Library/Application Support/com.dinoki.osaurus/SharedConfiguration/
These values come from the app’s configuration and are expected to remain stable.