Remove the [server.slatedb] settings and the SlateDB prefix probe

No store sits behind `[server.slatedb]` any more: the section leaves the
settings layer, the resolved server settings, the defaults, the API schema,
the TypeScript client, the install wizard and the docs, and `fabro install`
probes the bucket for the `artifacts/` prefix alone. A settings file that
still carries the section is rewritten once at startup by a temporary
migration that removes it with a backup beside the file.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-09-18 16:00:24 -04:00
parent ae70aa6ccf
commit 76461da780
No known key found for this signature in database
31 changed files with 286 additions and 359 deletions

View file

@ -142,7 +142,6 @@ petri_testkit = { git = "https://github.com/lithoscomputer/petri.git", rev = "4d
sentry = { version = "0.35", default-features = false, features = ["backtrace", "contexts", "ureq", "rustls"] }
fork = "0.2"
exec = "0.3"
slatedb = { version = "0.11.2", features = ["zstd"] }
object_store = { version = "0.12.5", features = ["aws"] }
rust-embed = "8"
percent-encoding = "2"

View file

@ -555,7 +555,7 @@ describe("InstallApp", () => {
expect(text).toContain("fabro-data");
expect(text).toContain("us-east-1");
expect(text).toContain("Access key");
expect(text).toContain("slatedb/, artifacts/");
expect(text).toContain("artifacts/");
});
await act(async () => {

View file

@ -773,7 +773,7 @@ function ObjectStoreStep({
return (
<StepPanel
title="Choose the shared object store"
description="This configures the shared backend for both SlateDB and run artifacts. Fabro still keeps its local storage root on disk."
description="This configures the backend for run artifacts. Fabro still keeps its local storage root on disk."
error={saveError}
submitting={submitting}
submittingLabel={
@ -969,7 +969,7 @@ function ObjectStoreStep({
<div className="space-y-3">
<Field
label="Local directory"
hint="Shared root for SlateDB and run artifacts."
hint="Root for run artifacts."
>
<input
ref={localRootInputRef}
@ -989,7 +989,7 @@ function ObjectStoreStep({
/>
</Field>
<p className="rounded-lg bg-overlay px-4 py-3 text-sm/6 text-fg-3 outline-1 -outline-offset-1 outline-white/10">
Fabro will store SlateDB and run artifacts under this directory.
Fabro will store run artifacts under this directory.
</p>
</div>
)}
@ -1556,7 +1556,7 @@ function WelcomeScreen() {
["Server URL", "Confirm where operators will reach Fabro."],
[
"Object store",
"Choose local disk or AWS S3 for SlateDB and artifacts.",
"Choose local disk or AWS S3 for artifacts.",
],
["Sandbox", "Choose Docker or Daytona for workflow execution."],
["LLMs", "Validate API keys for Anthropic, OpenAI, or Gemini."],
@ -1869,12 +1869,12 @@ const OBJECT_STORE_PROVIDER_OPTIONS: ReadonlyArray<CardOption<ObjectStoreProvide
{
id: "local",
title: "Local disk",
body: "Uses the host filesystem for SlateDB and run artifacts.",
body: "Uses the host filesystem for run artifacts.",
},
{
id: "s3",
title: "AWS S3",
body: "Uses one S3 bucket with fixed slatedb/ and artifacts/ prefixes.",
body: "Uses one S3 bucket with the fixed artifacts/ prefix.",
},
];
@ -2394,7 +2394,7 @@ function ObjectStoreSummaryRows({
: "Runtime credentials"
}
/>
<SummaryRow label="Prefixes" value="slatedb/, artifacts/" mono />
<SummaryRow label="Prefix" value="artifacts/" mono />
</>
);
}

View file

@ -38,14 +38,12 @@ export default function SettingsStorage() {
{settings && resources ? (
<>
<StorageRootPanel settings={settings} disk={resources.disk} />
<SlateDbPanel settings={settings} />
<ArtifactsPanel settings={settings} />
</>
) : (
<>
<PanelSkeleton />
<PanelSkeleton />
<PanelSkeleton />
</>
)}
</div>
@ -75,15 +73,6 @@ function StorageRootPanel({
);
}
function SlateDbPanel({ settings }: { settings: ServerSettings }) {
const { slatedb } = settings.server;
return (
<Panel title="SlateDB">
<ObjectStoreRows store={slatedb.store} prefix={slatedb.prefix} />
</Panel>
);
}
function ArtifactsPanel({ settings }: { settings: ServerSettings }) {
const { artifacts } = settings.server;
return (

View file

@ -17,7 +17,7 @@ Fabro only reads `settings.toml`. Older `server.toml`, `user.toml`, and `cli.tom
| Scope | Examples |
|---|---|
| Server-owned (runtime-only from local `settings.toml`) | `[server.listen]`, `[server.api]`, `[server.web]`, `[server.auth]`, `[server.sandbox]`, `[server.storage]`, `[server.artifacts]`, `[server.slatedb]`, `[server.scheduler]`, `[server.logging]`, `[server.integrations]` |
| Server-owned (runtime-only from local `settings.toml`) | `[server.listen]`, `[server.api]`, `[server.web]`, `[server.auth]`, `[server.sandbox]`, `[server.storage]`, `[server.artifacts]`, `[server.scheduler]`, `[server.logging]`, `[server.integrations]` |
| Shared run defaults (layered through `.fabro/project.toml`/`workflow.toml`) | `[run.model]`, `[run.prepare]`, `[run.environment]`, `[environments.<slug>]`, `[run.checkpoint]`, `[run.inputs]`, `[run.pull_request]`, `[run.git]`, `[run.hooks]`, `[run.agent]` |
The CLI-only `[cli.*]` sections (including `[cli.target]`) belong in the client machine's `settings.toml`. They tell CLI commands how to reach a server. The server process does not read `[cli.*]` for its own binding or routing.
@ -73,15 +73,6 @@ prefix = "artifacts"
bucket = "my-fabro-data"
region = "us-east-1"
[server.slatedb]
provider = "s3"
prefix = "slatedb"
disk_cache = true
[server.slatedb.s3]
bucket = "my-fabro-data"
region = "us-east-1"
[server.scheduler]
max_concurrent_runs = 8
@ -226,40 +217,19 @@ E2B_API_URL = "https://api.e2b.example"
| `env` | Complete environment for the plugin, apart from `inherit_env` | `{}` |
| `inherit_env` | Ambient variables forwarded from the server process | `[]` |
### `[server.slatedb]` section
### Removed: `[server.slatedb]`
Configure the embedded SlateDB key-value store used for the remaining
object-store-backed indexes and as the read-only source for temporary storage
migrations. Run history and content-addressed blobs live in SQLite; artifacts
use `[server.artifacts]`.
Earlier releases kept an embedded SlateDB key-value store beside the
artifact store. Run history and content-addressed blobs now live in SQLite,
so the section has no store behind it. A `settings.toml` that still carries
`[server.slatedb]` is rewritten once at startup: the section is removed, a
backup is written beside the file, and a warning names both. Delete the
section yourself to avoid the rewrite.
| Key | Description | Default |
|---|---|---|
| `provider` | Object store backend: `local` or `s3` | `"local"` |
| `prefix` | Key prefix within the object store | `""` |
| `flush_interval` | How often to flush the write-ahead log | `"1ms"` |
| `disk_cache` | Enable a local disk cache for object store reads | `false` |
When `disk_cache = true`, Fabro creates a cache directory at `<storage_root>/cache/slatedb` and
configures SlateDB to cache object store bytes on local disk (16 GB max, 4 MB parts). This
significantly reduces read latency and costs for S3-backed deployments. A warning is emitted if
enabled with `provider = "local"` since the disk cache adds overhead when the object store is
already local.
```toml title="settings.toml"
[server.slatedb]
provider = "s3"
disk_cache = true
[server.slatedb.s3]
bucket = "fabro-production"
region = "us-east-1"
```
The browser install wizard's `Object store` step manages both `[server.slatedb]` and
`[server.artifacts]` together. `Local disk` uses the detected local object-store root, defaulting
to `<storage_root>/objects`, with fixed prefixes `slatedb` and `artifacts`. `AWS S3` writes one
shared bucket with the same fixed prefixes.
The browser install wizard's `Object store` step manages `[server.artifacts]`.
`Local disk` uses the detected local object-store root, defaulting to
`<storage_root>/objects`, with the fixed prefix `artifacts`. `AWS S3` writes
one bucket with the same fixed prefix.
```toml title="Local disk object store"
[server.artifacts]
@ -268,13 +238,6 @@ prefix = "artifacts"
[server.artifacts.local]
root = "/var/lib/fabro/objects"
[server.slatedb]
provider = "local"
prefix = "slatedb"
[server.slatedb.local]
root = "/var/lib/fabro/objects"
```
The wizard only covers AWS S3 bucket/region plus one of:
@ -284,16 +247,14 @@ The wizard only covers AWS S3 bucket/region plus one of:
Advanced S3-compatible settings such as custom `endpoint` or `path_style` remain a manual
configuration path. If you need MinIO, R2, or another S3-compatible backend, configure
`[server.slatedb]` and `[server.artifacts]` directly in `settings.toml`. The runtime still
honors those hand-edited values even though the browser wizard does not manage them.
`[server.artifacts]` directly in `settings.toml`. The runtime still honors those hand-edited
values even though the browser wizard does not manage them.
### SQLite state and migration backups
Shared relational state, including run events and current run rows,
Shared relational state, including run records and current run rows,
content-addressed blobs, vault entries, server-managed definitions, and CLI
auth sessions, lives at `<storage_root>/db/fabro.sqlite3`. The
`[server.slatedb]` object store remains configured for compatibility imports
and session-to-run reverse indexes during the storage transition.
auth sessions, lives at `<storage_root>/db/fabro.sqlite3`.
CLI auth sessions are stored as an `auth_sessions` row per signed-in CLI, with the rotating refresh tokens for that session in `refresh_tokens`. Pending browser-to-CLI handoffs live briefly in `oauth_authorization_codes`; the table contains a SHA-256 hash of each one-time code, never the raw bearer value. Revoking a session from **Settings → Sessions**, or with `DELETE /api/v1/auth/sessions/{id}`, deletes the session row and its tokens together.
@ -476,7 +437,7 @@ Fabro resolves these from `process env -> server.env`.
| Variable | Description |
|---|---|
| `AWS_ACCESS_KEY_ID` | Static AWS access key ID for S3-backed `[server.slatedb]` / `[server.artifacts]` |
| `AWS_ACCESS_KEY_ID` | Static AWS access key ID for an S3-backed `[server.artifacts]` |
| `AWS_SECRET_ACCESS_KEY` | Matching static AWS secret access key |
| `AWS_SESSION_TOKEN` | Optional matching AWS session token for temporary static credentials |
@ -484,8 +445,8 @@ The browser install wizard can write these into `server.env` for the AWS S3 manu
path. It does not support manual STS/session-token input; use runtime credentials instead for ECS,
EC2 instance profiles, IRSA, or web-identity flows.
For the narrowest production policy, scope access to one bucket and the `slatedb/` and
`artifacts/` prefixes with `s3:ListBucket` plus `s3:GetObject`, `s3:PutObject`, and
For the narrowest production policy, scope access to one bucket and the `artifacts/` prefix
with `s3:ListBucket` plus `s3:GetObject`, `s3:PutObject`, and
`s3:DeleteObject`. Prefer a dedicated IAM user or role for Fabro instead of reusing broad AWS
credentials.

View file

@ -14890,7 +14890,6 @@ components:
- sandbox
- storage
- artifacts
- slatedb
- scheduler
- logging
- integrations
@ -14909,8 +14908,6 @@ components:
$ref: "#/components/schemas/ServerStorageSettings"
artifacts:
$ref: "#/components/schemas/ServerArtifactsSettings"
slatedb:
$ref: "#/components/schemas/ServerSlateDbSettings"
scheduler:
$ref: "#/components/schemas/ServerSchedulerSettings"
logging:
@ -15048,19 +15045,6 @@ components:
store:
$ref: "#/components/schemas/ObjectStoreSettings"
ServerSlateDbSettings:
type: object
required: [prefix, store, flush_interval, disk_cache]
properties:
prefix:
type: string
store:
$ref: "#/components/schemas/ObjectStoreSettings"
flush_interval:
type: string
disk_cache:
type: boolean
ObjectStoreSettings:
oneOf:
- $ref: "#/components/schemas/ObjectStoreLocalSettings"

View file

@ -28,9 +28,9 @@ When Fabro can construct a direct install URL, the token is embedded in the URL
The `Object store` step offers two wizard-managed modes:
- `Local disk` for a host-local object-store root, detected by default and editable before continuing
- `AWS S3` for one shared bucket with fixed `slatedb/` and `artifacts/` prefixes
- `AWS S3` for one bucket with the fixed `artifacts/` prefix
The wizard's manual-credential path stores only `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` in `server.env`. It does not collect STS/session tokens or S3-compatible endpoint settings. If you need MinIO, Cloudflare R2, path-style options, or custom endpoints, finish install with local defaults and then edit `[server.slatedb]` / `[server.artifacts]` in `settings.toml` manually.
The wizard's manual-credential path stores only `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` in `server.env`. It does not collect STS/session tokens or S3-compatible endpoint settings. If you need MinIO, Cloudflare R2, path-style options, or custom endpoints, finish install with local defaults and then edit `[server.artifacts]` in `settings.toml` manually.
When you finish the wizard, the server writes `~/.fabro/settings.toml` and exits cleanly. Start it again to boot in configured mode:

View file

@ -36,7 +36,7 @@ Files that omit `_version` are treated as version `1`. The legacy top-level `ver
| CLI-only | `[cli.target]`, `[cli.auth]`, `[cli.exec]`, `[cli.output]`, `[cli.updates]`, `[cli.logging]` |
| Server-side run policy | `[run.model]`, `[run.environment]`, `[environments.<slug>]`, `[run.checkpoint]`, `[run.inputs]`, `[run.prepare]`, `[run.pull_request]`, `[run.integrations.github]`, `[run.hooks]`, `[run.agent.mcps]` |
| Shared LLM catalog | `[llm]`, a lithos-llm catalog overlay: `[llm.providers.<id>]`, `[llm.providers.<id>.models.<id>]`, and the agent harness under `metadata.agent` |
| Server-only | `[server.listen]`, `[server.api]`, `[server.web]`, `[server.auth]`, `[server.storage]`, `[server.artifacts]`, `[server.slatedb]`, `[server.scheduler]`, `[server.logging]`, `[server.integrations]` |
| Server-only | `[server.listen]`, `[server.api]`, `[server.web]`, `[server.auth]`, `[server.storage]`, `[server.artifacts]`, `[server.scheduler]`, `[server.logging]`, `[server.integrations]` |
`[cli.*]` and `[server.*]` stanzas are owner-specific: they are only consumed from `~/.fabro/settings.toml` (plus process-local flags and env overrides). The same stanzas in `.fabro/project.toml` or `workflow.toml` remain schema-valid but runtime-inert.

View file

@ -550,7 +550,7 @@ mod tests {
"os": "darwin",
"arch": "arm64",
"storage_dir": "/tmp/fabro-worker-auth",
"storage_engine": "slatedb",
"storage_engine": "sqlite",
"runs": { "total": 0, "active": 0 },
"uptime_secs": 42
}));

View file

@ -95,7 +95,7 @@ fn auth_login_refresh_logout_flow() {
"os": "darwin",
"arch": "arm64",
"storage_dir": "/tmp/fabro-auth-flow",
"storage_engine": "slatedb",
"storage_engine": "sqlite",
"runs": { "total": 0, "active": 0 },
"uptime_secs": 42
}));

View file

@ -849,7 +849,7 @@ pub(crate) async fn get_system_info(
"profile": option_env!("FABRO_BUILD_PROFILE"),
"os": std::env::consts::OS,
"arch": std::env::consts::ARCH,
"storage_engine": "slatedb",
"storage_engine": "sqlite",
"storage_dir": "/demo/fabro/storage",
"uptime_secs": 42,
"runs": { "total": 3, "active": 1 },

View file

@ -1270,32 +1270,26 @@ async fn validate_install_object_store_selection(
Some(&build_options),
)?;
let probe_prefix = |index: usize, prefix: &'static str| {
let object_store = &object_store;
async move {
let path = ObjectStorePath::from(prefix);
object_store
.list_with_delimiter(Some(&path))
.await
.map(|_| ())
.map_err(|err| (index, err))
}
};
// The bucket answers for the one prefix Fabro keeps objects under.
let probe = async {
tokio::try_join!(probe_prefix(0, "artifacts"), probe_prefix(1, "slatedb")).map(|_| ())
let path = ObjectStorePath::from("artifacts");
object_store
.list_with_delimiter(Some(&path))
.await
.map(|_| ())
};
match timeout(VALIDATION_TIMEOUT, probe).await {
Ok(Ok(())) => Ok(()),
Err(_) => bail!(VALIDATION_TIMEOUT_MSG),
Ok(Err((index, err))) => bail!(
Ok(Err(err)) => bail!(
"{}",
classify_object_store_validation_error(bucket, region, index, &err)
classify_object_store_validation_error(bucket, region, &err)
),
}
}
const PREFIX_ACCESS_ERROR_MSG: &str = "Fabro reached the bucket but could not verify access to slatedb/ and artifacts/. Validation requires bucket list access plus object access under both prefixes.";
const PREFIX_ACCESS_ERROR_MSG: &str = "Fabro reached the bucket but could not verify access to artifacts/. Validation requires bucket list access plus object access under that prefix.";
const VALIDATION_TIMEOUT_MSG: &str = "Timed out while checking S3 access. Verify the bucket, region, and network path, then try again.";
fn bucket_credentials_error(bucket: &str, region: &str) -> String {
@ -1305,16 +1299,9 @@ fn bucket_credentials_error(bucket: &str, region: &str) -> String {
fn classify_object_store_validation_error(
bucket: &str,
region: &str,
prefix_index: usize,
err: &object_store::Error,
) -> String {
let credentials_or_prefix_error = || {
if prefix_index == 0 {
bucket_credentials_error(bucket, region)
} else {
PREFIX_ACCESS_ERROR_MSG.to_string()
}
};
let credentials_or_prefix_error = || bucket_credentials_error(bucket, region);
match err {
object_store::Error::PermissionDenied { .. }
| object_store::Error::Unauthenticated { .. } => credentials_or_prefix_error(),
@ -2745,7 +2732,7 @@ AWS_WEB_IDENTITY_TOKEN_FILE=/tmp/fabro-web-identity-token\n",
};
assert_eq!(
classify_object_store_validation_error("fabro-data", "us-east-1", 0, &err),
classify_object_store_validation_error("fabro-data", "us-east-1", &err),
"Bucket fabro-data is not reachable in region us-east-1. Verify the AWS region and try again."
);
}

View file

@ -1345,12 +1345,6 @@ mod tests {
panic!("artifacts store should stay local");
};
assert_eq!(root, "/srv/fabro-storage/objects/artifacts");
let fabro_types::settings::ObjectStoreSettings::Local { root } =
&resolved.server_settings.server.slatedb.store
else {
panic!("slatedb store should stay local");
};
assert_eq!(root, "/srv/fabro-storage/objects/slatedb");
}
#[test]

View file

@ -874,10 +874,6 @@ mod tests {
local_store_root(&settings.server.artifacts.store),
storage_root.join("objects/artifacts")
);
assert_eq!(
local_store_root(&settings.server.slatedb.store),
storage_root.join("objects/slatedb")
);
}
#[test]

View file

@ -415,7 +415,6 @@ pub fn write_object_store_settings(
let root_table = root_table_mut(doc)?;
let server = ensure_table(root_table, "server")?;
write_local_store_settings(server, "artifacts", "artifacts", root)?;
write_local_store_settings(server, "slatedb", "slatedb", root)?;
}
Ok(InstallObjectStoreEnvPlan {
writes: Vec::new(),
@ -437,7 +436,6 @@ pub fn write_object_store_settings(
let root = root_table_mut(doc)?;
let server = ensure_table(root, "server")?;
write_s3_store_settings(server, "artifacts", "artifacts", bucket, region)?;
write_s3_store_settings(server, "slatedb", "slatedb", bucket, region)?;
let removals = object_store_env_removals();
let writes = match credential_mode {
@ -1327,32 +1325,7 @@ stale = "remove-me"
.and_then(toml::Value::as_str),
Some("/srv/fabro/objects")
);
assert_eq!(
server
.get("slatedb")
.and_then(toml::Value::as_table)
.and_then(|slatedb| slatedb.get("provider"))
.and_then(toml::Value::as_str),
Some("local")
);
assert_eq!(
server
.get("slatedb")
.and_then(toml::Value::as_table)
.and_then(|slatedb| slatedb.get("prefix"))
.and_then(toml::Value::as_str),
Some("slatedb")
);
assert_eq!(
server
.get("slatedb")
.and_then(toml::Value::as_table)
.and_then(|slatedb| slatedb.get("local"))
.and_then(toml::Value::as_table)
.and_then(|local| local.get("root"))
.and_then(toml::Value::as_str),
Some("/srv/fabro/objects")
);
assert!(server.get("slatedb").is_none());
assert!(plan.writes.is_empty());
assert_eq!(plan.removals.len(), 2);
}
@ -1381,14 +1354,7 @@ stale = "remove-me"
.and_then(toml::Value::as_str),
Some("artifacts")
);
assert_eq!(
server
.get("slatedb")
.and_then(toml::Value::as_table)
.and_then(|slatedb| slatedb.get("prefix"))
.and_then(toml::Value::as_str),
Some("slatedb")
);
assert!(server.get("slatedb").is_none());
assert!(plan.writes.is_empty());
}

View file

@ -297,11 +297,6 @@ fn main() {
"fabro_types::settings::server::ServerArtifactsSettings",
&[],
),
(
"ServerSlateDbSettings",
"fabro_types::settings::server::ServerSlateDbSettings",
&[],
),
(
"ObjectStoreSettings",
"fabro_types::settings::server::ObjectStoreSettings",

View file

@ -29,8 +29,7 @@ pub mod types {
ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings, ServerIntegrationsSettings,
ServerListenSettings, ServerLoggingSettings, ServerSandboxProviderSettings,
ServerSandboxProvidersSettings, ServerSandboxSettings, ServerSchedulerSettings,
ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings,
WebhookStrategy,
ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings, WebhookStrategy,
};
pub use fabro_types::settings::{McpTransport, ServerNamespace};
pub use fabro_types::status::{

View file

@ -0,0 +1,189 @@
//! Temporary compatibility migration: drop `[server.slatedb]` from a
//! settings file.
//!
//! Earlier releases kept an embedded SlateDB store beside the artifact
//! store and configured it under `[server.slatedb]`. Run history and blobs
//! live in SQLite now and the section has no store behind it, so the
//! settings layer no longer knows the key and would refuse the file. This
//! migration removes the section (and its `local` and `s3` subtables) once,
//! with a backup beside the file, and leaves every other key as it was.
//!
//! Delete this migration after `REMOVAL_DEADLINE`, once supported upgrade
//! windows no longer start from a release that wrote the section.
#![expect(
clippy::disallowed_methods,
reason = "temporary startup config migration uses synchronous file I/O before config is loaded"
)]
use std::io::Write;
use std::path::{Path, PathBuf};
use toml_edit::DocumentMut;
use crate::{Error, Result};
/// When this migration can be removed.
pub(crate) const REMOVAL_DEADLINE: &str = "2027-03-01";
#[derive(Debug)]
pub(crate) struct RemoveServerSlateDbReport {
pub(crate) contents: String,
pub(crate) warning: String,
#[cfg(test)]
backup_path: PathBuf,
}
/// Rewrite `path` without its `[server.slatedb]` section when it has one:
/// the backup is written first, then the file. `Ok(None)` when the file
/// has no such section, or is not TOML the layer could read anyway.
pub(crate) fn migrate_settings_path(
path: &Path,
contents: &str,
) -> Result<Option<RemoveServerSlateDbReport>> {
let Some(next_contents) = migrate_contents(contents) else {
return Ok(None);
};
let backup_path = write_next_backup(path, contents)?;
std::fs::write(path, &next_contents).map_err(|source| {
Error::other(format!(
"writing migrated settings file {}: {source}",
path.display()
))
})?;
let warning = format!(
"Removed the [server.slatedb] section from {}: Fabro no longer keeps a SlateDB store. Backup written to {}. This temporary compatibility migration will be removed after {REMOVAL_DEADLINE}.",
path.display(),
backup_path.display()
);
Ok(Some(RemoveServerSlateDbReport {
contents: next_contents,
warning,
#[cfg(test)]
backup_path,
}))
}
/// The contents without `[server.slatedb]`, or `None` when there is
/// nothing to remove.
pub(crate) fn migrate_contents(contents: &str) -> Option<String> {
let mut doc = contents.parse::<DocumentMut>().ok()?;
let server = doc.get_mut("server")?.as_table_like_mut()?;
server.remove("slatedb")?;
Some(doc.to_string())
}
fn write_next_backup(path: &Path, contents: &str) -> Result<PathBuf> {
for index in 0u32.. {
let backup_path = backup_path_for(path, index);
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&backup_path)
{
Ok(mut file) => {
file.write_all(contents.as_bytes()).map_err(|source| {
Error::other(format!(
"writing server.slatedb migration backup {}: {source}",
backup_path.display()
))
})?;
return Ok(backup_path);
}
Err(source) if source.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(source) => {
return Err(Error::other(format!(
"writing server.slatedb migration backup {}: {source}",
backup_path.display()
)));
}
}
}
unreachable!("unbounded backup suffix search should return")
}
fn backup_path_for(path: &Path, index: u32) -> PathBuf {
let file_name = path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("settings.toml");
if index == 0 {
path.with_file_name(format!("{file_name}.server-slatedb-migration.bak"))
} else {
path.with_file_name(format!("{file_name}.server-slatedb-migration.{index}.bak"))
}
}
#[cfg(test)]
mod tests {
use super::*;
const LEGACY: &str = r#"_version = 1
[server.artifacts]
provider = "local"
prefix = "artifacts"
# The store that is gone.
[server.slatedb]
provider = "s3"
prefix = "slatedb"
disk_cache = true
[server.slatedb.s3]
bucket = "fabro-data"
region = "us-east-1"
[server.scheduler]
max_concurrent_runs = 3
"#;
#[test]
fn a_file_without_the_section_is_left_alone() {
assert_eq!(
migrate_contents("_version = 1\n\n[server.web]\nenabled = true\n"),
None
);
assert_eq!(migrate_contents("not toml ["), None);
}
#[test]
fn the_section_and_its_subtables_go_and_the_rest_stays() {
let migrated = migrate_contents(LEGACY).expect("the section is removed");
assert!(!migrated.contains("slatedb"), "{migrated}");
assert!(migrated.contains("[server.artifacts]"), "{migrated}");
assert!(migrated.contains("prefix = \"artifacts\""), "{migrated}");
assert!(migrated.contains("max_concurrent_runs = 3"), "{migrated}");
assert_eq!(
migrate_contents(&migrated),
None,
"a second pass is a no-op"
);
}
#[test]
fn the_file_is_rewritten_with_a_backup_once() {
let dir = tempfile::tempdir().expect("a temp dir");
let path = dir.path().join("settings.toml");
std::fs::write(&path, LEGACY).expect("the legacy file");
let report = migrate_settings_path(&path, LEGACY)
.expect("the migration runs")
.expect("the file changed");
assert_eq!(
std::fs::read_to_string(&report.backup_path).expect("the backup"),
LEGACY
);
let rewritten = std::fs::read_to_string(&path).expect("the file");
assert_eq!(rewritten, report.contents);
assert!(!rewritten.contains("slatedb"));
assert!(report.warning.contains("[server.slatedb]"));
assert!(
migrate_settings_path(&path, &rewritten)
.expect("the migration runs")
.is_none(),
"the second run is a no-op"
);
}
}

View file

@ -42,9 +42,3 @@ max_concurrent_runs = 5
[server.artifacts]
provider = "local"
prefix = ""
[server.slatedb]
provider = "local"
prefix = ""
flush_interval = "1ms"
disk_cache = false

View file

@ -38,8 +38,7 @@ pub use server::{
ServerApiLayer, ServerArtifactsLayer, ServerAuthGithubLayer, ServerAuthLayer,
ServerIntegrationsLayer, ServerLayer, ServerListenLayer, ServerLoggingLayer,
ServerSandboxLayer, ServerSandboxProviderLayer, ServerSandboxProvidersLayer,
ServerSchedulerLayer, ServerSlateDbLayer, ServerStorageLayer, ServerWebLayer,
SlackIntegrationLayer,
ServerSchedulerLayer, ServerStorageLayer, ServerWebLayer, SlackIntegrationLayer,
};
pub use settings::SettingsLayer;
pub use workflow::WorkflowLayer;

View file

@ -3,11 +3,11 @@
use std::collections::BTreeMap;
use fabro_types::SandboxProviderKind;
use fabro_types::settings::InterpString;
use fabro_types::settings::server::{
GithubIntegrationStrategy, LogDestination, ObjectStoreProvider, ServerAuthMethod,
WebhookStrategy,
};
use fabro_types::settings::{Duration, InterpString};
use serde::{Deserialize, Serialize};
use super::LogFilter;
@ -31,8 +31,6 @@ pub struct ServerLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub artifacts: Option<ServerArtifactsLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub slatedb: Option<ServerSlateDbLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scheduler: Option<ServerSchedulerLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub logging: Option<ServerLoggingLayer>,
@ -167,24 +165,6 @@ pub struct ServerArtifactsLayer {
pub s3: Option<ObjectStoreS3Layer>,
}
/// `[server.slatedb]` — SlateDB bottomless storage plus tunables.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
#[serde(deny_unknown_fields)]
pub struct ServerSlateDbLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider: Option<ObjectStoreProvider>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prefix: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub flush_interval: Option<Duration>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub local: Option<ObjectStoreLocalLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub s3: Option<ObjectStoreS3Layer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disk_cache: Option<bool>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ObjectStoreLocalLayer {

View file

@ -54,8 +54,8 @@ pub use layers::{
ServerApiLayer, ServerArtifactsLayer, ServerAuthGithubLayer, ServerAuthLayer,
ServerIntegrationsLayer, ServerLayer, ServerListenLayer, ServerLoggingLayer,
ServerSandboxLayer, ServerSandboxProviderLayer, ServerSandboxProvidersLayer,
ServerSchedulerLayer, ServerSlateDbLayer, ServerStorageLayer, ServerWebLayer, SettingsLayer,
SlackIntegrationLayer, StickyMap, StringOrSplice, WorkflowLayer,
ServerSchedulerLayer, ServerStorageLayer, ServerWebLayer, SettingsLayer, SlackIntegrationLayer,
StickyMap, StringOrSplice, WorkflowLayer,
};
pub use logging::{resolve_log_destination, resolve_log_destination_with_env};
pub use parse::ParseError;

View file

@ -1,17 +1,54 @@
//! The settings-file migrations, in the order they run: the legacy
//! `[run.sandbox]` and `[environments]` rewrites first, then the removal of
//! `[server.slatedb]`. Each is state-driven and a no-op on a file already
//! in the current shape.
use std::path::Path;
use crate::Result;
#[path = "../migrations/2026050101_legacy_sandbox_to_environments.rs"]
mod legacy_sandbox_to_environments;
#[path = "../migrations/2026091801_remove_server_slatedb.rs"]
mod remove_server_slatedb;
#[path = "../migrations/2026052801_settings_environments_to_server_files.rs"]
mod settings_environments_to_server_files;
pub(crate) use settings_environments_to_server_files::SettingsEnvironmentsMigrationReport as MigrationReport;
/// What the migrations left: the file's contents now, and the warning that
/// names every rewrite.
#[derive(Debug)]
pub(crate) struct MigrationReport {
pub(crate) contents: String,
pub(crate) warning: String,
}
pub(crate) fn run_migrations(
path: &Path,
original_contents: &str,
) -> Result<Option<MigrationReport>> {
settings_environments_to_server_files::migrate_settings_path(path, original_contents)
let mut report: Option<MigrationReport> = None;
if let Some(environments) =
settings_environments_to_server_files::migrate_settings_path(path, original_contents)?
{
report = Some(MigrationReport {
contents: environments.contents,
warning: environments.warning,
});
}
let contents = report
.as_ref()
.map_or(original_contents, |report| report.contents.as_str());
if let Some(slatedb) = remove_server_slatedb::migrate_settings_path(path, contents)? {
report = Some(match report {
Some(earlier) => MigrationReport {
contents: slatedb.contents,
warning: format!("{} {}", earlier.warning, slatedb.warning),
},
None => MigrationReport {
contents: slatedb.contents,
warning: slatedb.warning,
},
});
}
Ok(report)
}

View file

@ -8,8 +8,8 @@ use fabro_types::settings::server::{
ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings,
ServerIntegrationsSettings, ServerListenSettings, ServerLoggingSettings, ServerNamespace,
ServerSandboxProviderSettings, ServerSandboxProvidersSettings, ServerSandboxSettings,
ServerSchedulerSettings, ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings,
SlackIntegrationSettings, WebhookStrategy,
ServerSchedulerSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings,
WebhookStrategy,
};
use fabro_util::Home;
@ -21,8 +21,7 @@ use crate::user::default_storage_dir;
use crate::{
IntegrationWebhooksLayer, ObjectStoreLocalLayer, ObjectStoreS3Layer, ServerApiLayer,
ServerArtifactsLayer, ServerAuthLayer, ServerIntegrationsLayer, ServerLayer, ServerListenLayer,
ServerSandboxLayer, ServerSandboxProviderLayer, ServerSlateDbLayer, ServerStorageLayer,
ServerWebLayer,
ServerSandboxLayer, ServerSandboxProviderLayer, ServerStorageLayer, ServerWebLayer,
};
pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec<ResolveError>) -> ServerNamespace {
@ -44,7 +43,6 @@ pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec<ResolveError>) -> Se
sandbox: resolve_sandbox(layer.sandbox.as_ref(), errors),
storage: storage.clone(),
artifacts: resolve_artifacts(layer.artifacts.as_ref(), &storage.root, errors),
slatedb: resolve_slatedb(layer.slatedb.as_ref(), &storage.root, errors),
scheduler: ServerSchedulerSettings {
max_concurrent_runs: layer
.scheduler
@ -294,50 +292,6 @@ fn resolve_artifacts(
}
}
fn resolve_slatedb(
layer: Option<&ServerSlateDbLayer>,
storage_root: &str,
errors: &mut Vec<ResolveError>,
) -> ServerSlateDbSettings {
let provider = layer
.and_then(|slatedb| slatedb.provider)
.expect("defaults.toml should provide server.slatedb.provider");
let disk_cache = layer
.and_then(|slatedb| slatedb.disk_cache)
.expect("defaults.toml should provide server.slatedb.disk_cache");
if disk_cache && provider == ObjectStoreProvider::Local {
tracing::warn!(
"disk_cache enabled with local provider; \
disk cache is designed for S3-backed deployments \
and adds overhead on local filesystems"
);
}
let prefix = layer
.and_then(|slatedb| slatedb.prefix.clone())
.expect("defaults.toml should provide server.slatedb.prefix");
warn_if_demoted_template("server.slatedb.prefix", Some(prefix.as_str()));
ServerSlateDbSettings {
prefix,
store: resolve_object_store(
provider,
layer.and_then(|slatedb| slatedb.local.as_ref()),
layer.and_then(|slatedb| slatedb.s3.as_ref()),
&object_store_default_root(storage_root, "slatedb"),
"server.slatedb",
errors,
),
flush_interval: layer
.and_then(|slatedb| slatedb.flush_interval)
.map(|duration| duration.as_std())
.expect("defaults.toml should provide server.slatedb.flush_interval"),
disk_cache,
}
}
fn resolve_object_store(
provider: ObjectStoreProvider,
local: Option<&ObjectStoreLocalLayer>,

View file

@ -34,11 +34,6 @@ impl Storage {
self.root.join("cache")
}
#[must_use]
pub fn slatedb_cache_dir(&self) -> PathBuf {
self.cache_dir().join("slatedb")
}
#[must_use]
pub fn secrets_path(&self) -> PathBuf {
self.root
@ -77,11 +72,6 @@ impl Storage {
self.root.join("objects")
}
#[must_use]
pub fn slatedb_dir(&self) -> PathBuf {
self.objects_dir().join("slatedb")
}
#[must_use]
pub fn artifacts_dir(&self) -> PathBuf {
self.objects_dir().join("artifacts")
@ -185,10 +175,6 @@ mod tests {
storage.cache_dir(),
std::path::Path::new("/tmp/fabro-data/cache")
);
assert_eq!(
storage.slatedb_cache_dir(),
std::path::Path::new("/tmp/fabro-data/cache/slatedb")
);
assert_eq!(
storage.secrets_path(),
std::path::Path::new("/tmp/fabro-data/vaults/default/secrets.json")
@ -205,10 +191,6 @@ mod tests {
storage.objects_dir(),
std::path::Path::new("/tmp/fabro-data/objects")
);
assert_eq!(
storage.slatedb_dir(),
std::path::Path::new("/tmp/fabro-data/objects/slatedb")
);
assert_eq!(
storage.artifacts_dir(),
std::path::Path::new("/tmp/fabro-data/objects/artifacts")

View file

@ -89,21 +89,6 @@ fn resolves_server_defaults_from_empty_settings() {
ObjectStoreSettings::S3 { .. } => panic!("expected local artifact store by default"),
}
assert_eq!(settings.artifacts.prefix, "");
match settings.slatedb.store {
ObjectStoreSettings::Local { root } => {
assert_eq!(
root,
default_storage_dir()
.join("objects")
.join("slatedb")
.to_string_lossy()
);
}
ObjectStoreSettings::S3 { .. } => panic!("expected local slatedb store by default"),
}
assert!(!settings.slatedb.disk_cache);
}
#[test]
@ -542,22 +527,6 @@ enabled = true
);
}
#[test]
fn resolves_disk_cache_true_from_settings() {
let file = parse(
r"
_version = 1
[server.slatedb]
disk_cache = true
",
);
let settings = resolve_server(&file);
assert!(settings.slatedb.disk_cache);
}
#[test]
fn parsing_rejects_removed_server_ip_allowlist() {
let err = r#"

View file

@ -18,7 +18,6 @@ impl ServerSettings {
pub fn with_storage_override(mut self, path: &Path) -> Self {
self.server.storage.root = path.display().to_string();
override_local_object_store_root(&mut self.server.artifacts.store, path, "artifacts");
override_local_object_store_root(&mut self.server.slatedb.store, path, "slatedb");
self
}
}

View file

@ -48,8 +48,8 @@ pub use server::{
GithubIntegrationSettings, IntegrationWebhooksSettings, LogDestination, ObjectStoreSettings,
ServerApiSettings, ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod,
ServerAuthSettings, ServerIntegrationsSettings, ServerListenSettings, ServerLoggingSettings,
ServerNamespace, ServerSchedulerSettings, ServerSlateDbSettings, ServerStorageSettings,
ServerWebSettings, SlackIntegrationSettings,
ServerNamespace, ServerSchedulerSettings, ServerStorageSettings, ServerWebSettings,
SlackIntegrationSettings,
};
pub use size::{ParseSizeError, Size};
pub use workflow::WorkflowNamespace;

View file

@ -1,18 +1,16 @@
//! Server domain.
//!
//! `[server]` is a namespace container; actual settings live in named
//! subdomains (listen, api, web, auth, storage, artifacts, slatedb,
//! scheduler, logging, integrations). Same-host and split-host
//! subdomains (listen, api, web, auth, storage, artifacts, scheduler,
//! logging, integrations). Same-host and split-host
//! deployments use the same schema.
use std::collections::BTreeMap;
use std::net::SocketAddr;
use std::time::Duration as StdDuration;
use serde::de::Error as _;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use super::duration::Duration;
use crate::SandboxProviderKind;
/// A structurally resolved `[server]` view for consumers.
@ -31,7 +29,6 @@ pub struct ServerNamespace {
pub sandbox: ServerSandboxSettings,
pub storage: ServerStorageSettings,
pub artifacts: ServerArtifactsSettings,
pub slatedb: ServerSlateDbSettings,
pub scheduler: ServerSchedulerSettings,
pub logging: ServerLoggingSettings,
pub integrations: ServerIntegrationsSettings,
@ -52,7 +49,6 @@ impl ServerNamespace {
sandbox: ServerSandboxSettings::default(),
storage: ServerStorageSettings::default(),
artifacts: ServerArtifactsSettings::default(),
slatedb: ServerSlateDbSettings::default(),
scheduler: ServerSchedulerSettings::default(),
logging: ServerLoggingSettings::default(),
integrations: ServerIntegrationsSettings::default(),
@ -220,29 +216,6 @@ pub struct ServerArtifactsSettings {
pub store: ObjectStoreSettings,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServerSlateDbSettings {
pub prefix: String,
pub store: ObjectStoreSettings,
#[serde(
serialize_with = "serialize_std_duration",
deserialize_with = "deserialize_std_duration"
)]
pub flush_interval: StdDuration,
pub disk_cache: bool,
}
impl Default for ServerSlateDbSettings {
fn default() -> Self {
Self {
prefix: String::new(),
store: ObjectStoreSettings::default(),
flush_interval: StdDuration::ZERO,
disk_cache: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ObjectStoreSettings {
@ -348,20 +321,6 @@ where
value.parse().map_err(D::Error::custom)
}
fn serialize_std_duration<S>(value: &StdDuration, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&Duration::from_std(*value).to_string())
}
fn deserialize_std_duration<'de, D>(deserializer: D) -> Result<StdDuration, D::Error>
where
D: Deserializer<'de>,
{
Ok(Duration::deserialize(deserializer)?.as_std())
}
/// Closed enum of object-store providers. Unknown providers hard-fail
/// against the schema rather than passing through as opaque strings.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]

View file

@ -457,7 +457,6 @@ export * from './server-sandbox-provider-settings';
export * from './server-sandbox-settings';
export * from './server-scheduler-settings';
export * from './server-settings';
export * from './server-slate-db-settings';
export * from './server-storage-settings';
export * from './server-web-settings';
export * from './session-detail';

View file

@ -39,9 +39,6 @@ import type { ServerSandboxSettings } from './server-sandbox-settings';
import type { ServerSchedulerSettings } from './server-scheduler-settings';
// May contain unused imports in some cases
// @ts-ignore
import type { ServerSlateDbSettings } from './server-slate-db-settings';
// May contain unused imports in some cases
// @ts-ignore
import type { ServerStorageSettings } from './server-storage-settings';
// May contain unused imports in some cases
// @ts-ignore
@ -55,7 +52,6 @@ export interface ServerNamespace {
'sandbox': ServerSandboxSettings;
'storage': ServerStorageSettings;
'artifacts': ServerArtifactsSettings;
'slatedb': ServerSlateDbSettings;
'scheduler': ServerSchedulerSettings;
'logging': ServerLoggingSettings;
'integrations': ServerIntegrationsSettings;