Rename setups and add provider reauthentication
This commit is contained in:
1 parent
e9a0f1b9da
commit
7d9df5d572
36 files changed
+1866
-684
No files matched your search
@@ -28,7 +28,7 @@ class ApiException(message: String, val status: Int? = null, cause: Throwable? =
|
||||
Exception(message, cause)
|
||||
|
||||
/**
|
||||
* Runs one request against the backend, with the pinned TLS setup, the bearer token, and the
|
||||
* Runs one request against the backend, with the pinned TLS machine, the bearer token, and the
|
||||
* failure translation every call needs. [readBody] gets the connected, already-status-checked
|
||||
* connection.
|
||||
*
|
||||
@@ -122,12 +122,12 @@ data class SessionSummary(
|
||||
val id: String,
|
||||
/**
|
||||
* Id of the machine this session runs on. Only ever used to *address* that machine -- to pick
|
||||
* this session's row out of the per-machine usage snapshots. Never shown; [setupName] is what a
|
||||
* reader sees, and holding both invites showing the wrong one.
|
||||
* this session's row out of the per-machine usage snapshots. Never shown; [machineName] is what
|
||||
* a reader sees, and holding both invites showing the wrong one.
|
||||
*/
|
||||
val setup: String,
|
||||
/** The machine's current label. This is the one to display; [setup] is never shown. */
|
||||
val setupName: String,
|
||||
val machine: String,
|
||||
/** The machine's current label. This is the one to display; [machine] is never shown. */
|
||||
val machineName: String,
|
||||
val provider: String,
|
||||
val title: String,
|
||||
val model: String?,
|
||||
@@ -238,10 +238,10 @@ data class SessionSummary(
|
||||
private fun parseSession(session: JSONObject) =
|
||||
SessionSummary(
|
||||
id = session.getString("id"),
|
||||
setup = session.getString("setup"),
|
||||
machine = session.getString("machine"),
|
||||
keepsOwnTranscript = session.optBoolean("keepsOwnTranscript", false),
|
||||
ownTranscriptName = session.optString("ownTranscriptName").ifEmpty { null },
|
||||
setupName = session.getString("setupName"),
|
||||
machineName = session.getString("machineName"),
|
||||
provider = session.getString("provider"),
|
||||
title = session.getString("title"),
|
||||
model = session.optString("model").ifEmpty { null },
|
||||
@@ -328,7 +328,8 @@ fun deleteSubagents(settings: ServerSettings, sessionId: String, subagentIds: Li
|
||||
) {}
|
||||
}
|
||||
|
||||
// What the server offers, so the spawn screen has no hardcoded lists: a setup added to the server's
|
||||
// What the server offers, so the spawn screen has no hardcoded lists: a machine added to the
|
||||
// server's
|
||||
// config.ron appears here with no app rebuild.
|
||||
//
|
||||
// One list rather than two. A provider only exists on a machine that has it installed, so offering
|
||||
@@ -345,9 +346,9 @@ data class Provider(
|
||||
* A machine, and what it can run. [address] is absent for the backend itself.
|
||||
*
|
||||
* [id] is stable and [name] is not: renaming a machine keeps its sessions, so everything that
|
||||
* refers to a setup uses the id and everything a person reads uses the name.
|
||||
* refers to a machine uses the id and everything a person reads uses the name.
|
||||
*/
|
||||
data class Setup(
|
||||
data class Machine(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val address: String?,
|
||||
@@ -366,16 +367,91 @@ private fun parseProvider(provider: JSONObject): Provider {
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseSetup(setup: JSONObject) =
|
||||
Setup(
|
||||
id = setup.getString("id"),
|
||||
name = setup.getString("name"),
|
||||
address = setup.optString("address").ifEmpty { null },
|
||||
providers = setup.getJSONArray("providers").mapObjects(::parseProvider),
|
||||
private fun parseMachine(machine: JSONObject) =
|
||||
Machine(
|
||||
id = machine.getString("id"),
|
||||
name = machine.getString("name"),
|
||||
address = machine.optString("address").ifEmpty { null },
|
||||
providers = machine.getJSONArray("providers").mapObjects(::parseProvider),
|
||||
)
|
||||
|
||||
fun fetchSetups(settings: ServerSettings): List<Setup> =
|
||||
requestFromServer(settings, "/setups") { it.jsonObjects(::parseSetup) }
|
||||
fun fetchMachines(settings: ServerSettings): List<Machine> =
|
||||
requestFromServer(settings, "/machines") { it.jsonObjects(::parseMachine) }
|
||||
|
||||
/** One CLI-owned provider sign-in. The browser URL and pasted code are never persisted. */
|
||||
data class ProviderLogin(
|
||||
val attempt: String,
|
||||
val state: String,
|
||||
val authorizationUrl: String?,
|
||||
val detail: String?,
|
||||
)
|
||||
|
||||
private fun parseProviderLogin(login: JSONObject) =
|
||||
ProviderLogin(
|
||||
attempt = login.getString("attempt"),
|
||||
state = login.getString("state"),
|
||||
authorizationUrl = login.optString("authorizationUrl").ifEmpty { null },
|
||||
detail = login.optString("detail").ifEmpty { null },
|
||||
)
|
||||
|
||||
private fun providerLoginPath(machine: String, provider: String) =
|
||||
"/machines/${machine.urlEncoded()}/providers/${provider.urlEncoded()}/auth"
|
||||
|
||||
fun startProviderLogin(
|
||||
settings: ServerSettings,
|
||||
machine: String,
|
||||
provider: String,
|
||||
): ProviderLogin =
|
||||
requestFromServer(
|
||||
settings,
|
||||
providerLoginPath(machine, provider),
|
||||
method = "POST",
|
||||
readTimeoutMs = 25_000,
|
||||
) {
|
||||
parseProviderLogin(it.jsonObject())
|
||||
}
|
||||
|
||||
fun fetchProviderLogin(
|
||||
settings: ServerSettings,
|
||||
machine: String,
|
||||
provider: String,
|
||||
attempt: String,
|
||||
): ProviderLogin =
|
||||
requestFromServer(
|
||||
settings,
|
||||
"${providerLoginPath(machine, provider)}/${attempt.urlEncoded()}",
|
||||
) {
|
||||
parseProviderLogin(it.jsonObject())
|
||||
}
|
||||
|
||||
fun submitProviderLoginCode(
|
||||
settings: ServerSettings,
|
||||
machine: String,
|
||||
provider: String,
|
||||
attempt: String,
|
||||
code: String,
|
||||
): ProviderLogin =
|
||||
requestFromServer(
|
||||
settings,
|
||||
"${providerLoginPath(machine, provider)}/${attempt.urlEncoded()}/code",
|
||||
method = "POST",
|
||||
jsonBody = JSONObject().put("code", code).toString(),
|
||||
) {
|
||||
parseProviderLogin(it.jsonObject())
|
||||
}
|
||||
|
||||
fun cancelProviderLogin(
|
||||
settings: ServerSettings,
|
||||
machine: String,
|
||||
provider: String,
|
||||
attempt: String,
|
||||
) {
|
||||
requestFromServer(
|
||||
settings,
|
||||
"${providerLoginPath(machine, provider)}/${attempt.urlEncoded()}",
|
||||
method = "DELETE",
|
||||
) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* A Claude Code session already on a machine, which can be continued here.
|
||||
@@ -426,7 +502,7 @@ data class Importable(
|
||||
)
|
||||
|
||||
/**
|
||||
* One frame of `GET /setups/{id}/importable/events`: an operation starting, finishing or failing.
|
||||
* One frame of `GET /machines/{id}/importable/events`: an operation starting, finishing or failing.
|
||||
*
|
||||
* [operation] is only set by a start and [message] only by a failure -- the three states are every
|
||||
* way an operation can be, and each carries exactly what that state knows.
|
||||
@@ -461,8 +537,8 @@ fun parseImportableChange(payload: String): ImportableChange? =
|
||||
* reading every transcript Claude Code has ever written: about four seconds against a gigabyte of
|
||||
* them before the tunnel adds anything. A timeout is for a server that has stopped answering.
|
||||
*/
|
||||
fun fetchImportable(settings: ServerSettings, setup: String): List<Importable> =
|
||||
requestFromServer(settings, "/setups/$setup/importable", readTimeoutMs = 60000) {
|
||||
fun fetchImportable(settings: ServerSettings, machine: String): List<Importable> =
|
||||
requestFromServer(settings, "/machines/$machine/importable", readTimeoutMs = 60000) {
|
||||
it.jsonObjects { session ->
|
||||
Importable(
|
||||
id = session.getString("id"),
|
||||
@@ -515,10 +591,10 @@ private fun SshDetails.toJson() =
|
||||
}
|
||||
|
||||
/** What a machine turns out to have, without saving anything. */
|
||||
fun probeSetup(settings: ServerSettings, ssh: SshDetails?): List<Provider> =
|
||||
fun probeMachine(settings: ServerSettings, ssh: SshDetails?): List<Provider> =
|
||||
requestFromServer(
|
||||
settings,
|
||||
"/setups/probe",
|
||||
"/machines/probe",
|
||||
method = "POST",
|
||||
jsonBody = JSONObject().apply { if (ssh != null) put("ssh", ssh.toJson()) }.toString(),
|
||||
readTimeoutMs = 40000,
|
||||
@@ -526,10 +602,10 @@ fun probeSetup(settings: ServerSettings, ssh: SshDetails?): List<Provider> =
|
||||
it.jsonObjects(::parseProvider)
|
||||
}
|
||||
|
||||
fun addSetup(settings: ServerSettings, name: String, ssh: SshDetails?): Setup =
|
||||
fun addMachine(settings: ServerSettings, name: String, ssh: SshDetails?): Machine =
|
||||
requestFromServer(
|
||||
settings,
|
||||
"/setups",
|
||||
"/machines",
|
||||
method = "POST",
|
||||
jsonBody =
|
||||
JSONObject()
|
||||
@@ -538,19 +614,19 @@ fun addSetup(settings: ServerSettings, name: String, ssh: SshDetails?): Setup =
|
||||
.toString(),
|
||||
readTimeoutMs = 40000,
|
||||
) {
|
||||
parseSetup(it.jsonObject())
|
||||
parseMachine(it.jsonObject())
|
||||
}
|
||||
|
||||
/** Renames a machine, and optionally asks it again what it has. */
|
||||
fun updateSetup(
|
||||
fun updateMachine(
|
||||
settings: ServerSettings,
|
||||
id: String,
|
||||
name: String? = null,
|
||||
rediscover: Boolean = false,
|
||||
): Setup =
|
||||
): Machine =
|
||||
requestFromServer(
|
||||
settings,
|
||||
"/setups/${id.urlEncoded()}",
|
||||
"/machines/${id.urlEncoded()}",
|
||||
method = "PUT",
|
||||
jsonBody =
|
||||
JSONObject()
|
||||
@@ -561,20 +637,20 @@ fun updateSetup(
|
||||
.toString(),
|
||||
readTimeoutMs = 40000,
|
||||
) {
|
||||
parseSetup(it.jsonObject())
|
||||
parseMachine(it.jsonObject())
|
||||
}
|
||||
|
||||
fun deleteSetup(settings: ServerSettings, id: String) {
|
||||
requestFromServer(settings, "/setups/${id.urlEncoded()}", method = "DELETE") {}
|
||||
fun deleteMachine(settings: ServerSettings, id: String) {
|
||||
requestFromServer(settings, "/machines/${id.urlEncoded()}", method = "DELETE") {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawns a session and returns it as the list would show it. [setup] names the machine and
|
||||
* Spawns a session and returns it as the list would show it. [machine] names the machine and
|
||||
* [provider] one of the things that machine offers.
|
||||
*/
|
||||
fun spawnSession(
|
||||
settings: ServerSettings,
|
||||
setup: String,
|
||||
machine: String,
|
||||
provider: String,
|
||||
title: String,
|
||||
model: String? = null,
|
||||
@@ -592,7 +668,7 @@ fun spawnSession(
|
||||
method = "POST",
|
||||
jsonBody =
|
||||
JSONObject()
|
||||
.put("setup", setup)
|
||||
.put("machine", machine)
|
||||
.put("provider", provider)
|
||||
.put("title", title)
|
||||
.apply {
|
||||
@@ -705,7 +781,7 @@ fun uploadAttachment(
|
||||
}
|
||||
|
||||
/**
|
||||
* One entry of a directory on the machine a setup names.
|
||||
* One entry of a directory on a configured machine.
|
||||
*
|
||||
* [kind] is the *target's* where the entry is a symlink, so a link to a directory descends; [link]
|
||||
* still says it is one. Neither is worked out here -- the machine answers both.
|
||||
@@ -762,11 +838,11 @@ sealed class FileContent {
|
||||
/** What a file is after a write, so the editor's precondition is fresh without a second read. */
|
||||
data class Written(val size: Long, val modified: Long, val sha256: String)
|
||||
|
||||
/** Everything in [path] on the machine [setup] names, and what [path] resolved to. */
|
||||
fun fetchDir(settings: ServerSettings, setup: String, path: String): Listing =
|
||||
/** Everything in [path] on the machine [machine] names, and what [path] resolved to. */
|
||||
fun fetchDir(settings: ServerSettings, machine: String, path: String): Listing =
|
||||
requestFromServer(
|
||||
settings,
|
||||
"/setups/${setup.urlEncoded()}/dir?path=${path.urlEncoded()}",
|
||||
"/machines/${machine.urlEncoded()}/dir?path=${path.urlEncoded()}",
|
||||
readTimeoutMs = 30000,
|
||||
) { connection ->
|
||||
val body = connection.jsonObject()
|
||||
@@ -786,10 +862,10 @@ fun fetchDir(settings: ServerSettings, setup: String, path: String): Listing =
|
||||
}
|
||||
|
||||
/** One file's content, or which of the reasons there is none to show. */
|
||||
fun fetchFile(settings: ServerSettings, setup: String, path: String): FileContent =
|
||||
fun fetchFile(settings: ServerSettings, machine: String, path: String): FileContent =
|
||||
requestFromServer(
|
||||
settings,
|
||||
"/setups/${setup.urlEncoded()}/file?path=${path.urlEncoded()}",
|
||||
"/machines/${machine.urlEncoded()}/file?path=${path.urlEncoded()}",
|
||||
// A megabyte over the tunnel, and a `stat` plus a `sha256sum` on the far machine before any
|
||||
// of it moves. Well clear of that rather than just above it.
|
||||
readTimeoutMs = 60000,
|
||||
@@ -826,14 +902,14 @@ fun fetchFile(settings: ServerSettings, setup: String, path: String): FileConten
|
||||
*/
|
||||
fun writeFile(
|
||||
settings: ServerSettings,
|
||||
setup: String,
|
||||
machine: String,
|
||||
path: String,
|
||||
content: String,
|
||||
ifSha256: String,
|
||||
): Written =
|
||||
requestFromServer(
|
||||
settings,
|
||||
"/setups/${setup.urlEncoded()}/file",
|
||||
"/machines/${machine.urlEncoded()}/file",
|
||||
method = "PUT",
|
||||
jsonBody =
|
||||
JSONObject()
|
||||
@@ -848,10 +924,10 @@ fun writeFile(
|
||||
}
|
||||
|
||||
/** Creates an empty file. Refused, with the machine's own words, if the name is already taken. */
|
||||
fun createFile(settings: ServerSettings, setup: String, path: String) {
|
||||
fun createFile(settings: ServerSettings, machine: String, path: String) {
|
||||
requestFromServer(
|
||||
settings,
|
||||
"/setups/${setup.urlEncoded()}/file",
|
||||
"/machines/${machine.urlEncoded()}/file",
|
||||
method = "POST",
|
||||
jsonBody = JSONObject().put("path", path).toString(),
|
||||
readTimeoutMs = 30000,
|
||||
@@ -859,10 +935,10 @@ fun createFile(settings: ServerSettings, setup: String, path: String) {
|
||||
}
|
||||
|
||||
/** Creates a directory, with the same refusal as [createFile]. */
|
||||
fun createDir(settings: ServerSettings, setup: String, path: String) {
|
||||
fun createDir(settings: ServerSettings, machine: String, path: String) {
|
||||
requestFromServer(
|
||||
settings,
|
||||
"/setups/${setup.urlEncoded()}/dir",
|
||||
"/machines/${machine.urlEncoded()}/dir",
|
||||
method = "POST",
|
||||
jsonBody = JSONObject().put("path", path).toString(),
|
||||
readTimeoutMs = 30000,
|
||||
@@ -893,22 +969,23 @@ data class UsageWindow(
|
||||
data class UsageSnapshot(
|
||||
val provider: String,
|
||||
/** Stable id of the machine these numbers belong to. */
|
||||
val setup: String,
|
||||
val machine: String,
|
||||
/** That machine's current label. */
|
||||
val setupName: String,
|
||||
val machineName: String,
|
||||
/** Provider-specific billing pool, such as Codex's regular or Luna Reserve pool. */
|
||||
val limitId: String?,
|
||||
/** Provider-specific human-facing pool name, when supplied. */
|
||||
val limitName: String?,
|
||||
/**
|
||||
* What came back: "ok", "notLoggedIn", "unreachable" or "failed".
|
||||
* What came back: "ok", "notLoggedIn", "authenticating", "loginRequired", "unreachable" or
|
||||
* "failed".
|
||||
*
|
||||
* Four rather than a flag, because the screen has to treat them differently. "notLoggedIn" is a
|
||||
* machine somebody chose not to put an account on -- a fact, not a fault. Collapsing them made
|
||||
* a healthy setup read as broken.
|
||||
* Named states rather than a flag, because the screen has to treat them differently.
|
||||
* "notLoggedIn" is a machine somebody chose not to put an account on -- a fact, not a fault.
|
||||
* Collapsing them made a healthy machine read as broken.
|
||||
*/
|
||||
val state: String,
|
||||
/** Why, for the two states that are faults. Absent otherwise. */
|
||||
/** Why, for states that have a useful explanation. Absent otherwise. */
|
||||
val detail: String?,
|
||||
val windows: List<UsageWindow>,
|
||||
)
|
||||
@@ -919,8 +996,8 @@ fun fetchUsage(settings: ServerSettings): List<UsageSnapshot> =
|
||||
connection.jsonObjects { snapshot ->
|
||||
UsageSnapshot(
|
||||
provider = snapshot.getString("provider"),
|
||||
setup = snapshot.optString("setup"),
|
||||
setupName = snapshot.optString("setupName"),
|
||||
machine = snapshot.optString("machine"),
|
||||
machineName = snapshot.optString("machineName"),
|
||||
limitId = snapshot.optString("limitId").ifEmpty { null },
|
||||
limitName = snapshot.optString("limitName").ifEmpty { null },
|
||||
// Unknown to an older backend, and unknown is not "fine": defaulting to "ok" would
|
||||
@@ -1000,10 +1077,10 @@ fun startSession(settings: ServerSettings, sessionId: String) {
|
||||
* One request for the whole batch, which is what makes a handover all-or-nothing. One per row meant
|
||||
* a batch could half-arrive, and the rows that were missed looked exactly like rows not picked.
|
||||
*/
|
||||
fun deleteImportable(settings: ServerSettings, setup: String, sessionIds: List<String>) {
|
||||
fun deleteImportable(settings: ServerSettings, machine: String, sessionIds: List<String>) {
|
||||
requestFromServer(
|
||||
settings,
|
||||
"/setups/$setup/importable/delete",
|
||||
"/machines/$machine/importable/delete",
|
||||
method = "POST",
|
||||
jsonBody = JSONObject().put("sessions", JSONArray(sessionIds)).toString(),
|
||||
) {}
|
||||
@@ -1019,7 +1096,7 @@ fun deleteImportable(settings: ServerSettings, setup: String, sessionIds: List<S
|
||||
*/
|
||||
fun startImport(
|
||||
settings: ServerSettings,
|
||||
setup: String,
|
||||
machine: String,
|
||||
sessionIds: List<String>,
|
||||
provider: String,
|
||||
permissionMode: String? = null,
|
||||
@@ -1034,7 +1111,7 @@ fun startImport(
|
||||
}
|
||||
requestFromServer(
|
||||
settings,
|
||||
"/setups/$setup/importable/import",
|
||||
"/machines/$machine/importable/import",
|
||||
method = "POST",
|
||||
jsonBody = body.toString(),
|
||||
) {}
|
||||
@@ -1297,8 +1374,8 @@ private fun parseDownload(o: JSONObject) =
|
||||
* offering the backend's would name files that are not there, turning a choice that cannot work
|
||||
* into a session that fails when it tries to load one.
|
||||
*/
|
||||
fun fetchSetupModels(settings: ServerSettings, setupId: String): List<LocalModel> =
|
||||
requestFromServer(settings, "/setups/${setupId.urlEncoded()}/models") { connection ->
|
||||
fun fetchMachineModels(settings: ServerSettings, machineId: String): List<LocalModel> =
|
||||
requestFromServer(settings, "/machines/${machineId.urlEncoded()}/models") { connection ->
|
||||
JSONArray(connection.inputStream.bufferedReader().readText()).mapObjects { m ->
|
||||
LocalModel(
|
||||
key = m.getString("key"),
|
||||
@@ -1312,12 +1389,12 @@ fun fetchSetupModels(settings: ServerSettings, setupId: String): List<LocalModel
|
||||
/** The current model catalog for one CLI provider on the machine where it runs. */
|
||||
fun fetchProviderModels(
|
||||
settings: ServerSettings,
|
||||
setupId: String,
|
||||
machineId: String,
|
||||
provider: String,
|
||||
): List<String> =
|
||||
requestFromServer(
|
||||
settings,
|
||||
"/setups/${setupId.urlEncoded()}/providers/${provider.urlEncoded()}/models",
|
||||
"/machines/${machineId.urlEncoded()}/providers/${provider.urlEncoded()}/models",
|
||||
) { connection ->
|
||||
JSONArray(connection.inputStream.bufferedReader().readText()).strings()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user