Add the slopo CLI adapter

Three things live here: where the plugin keeps its files, how it renders a
slopo config, and how it finds, installs and invokes the binary.

The adapter is driven by behaviour observed against slopo 0.8.0 rather than by
its documentation:

  * --config is a global option and must precede the subcommand
  * every path is written absolute, because slopo resolves relative paths
    against the process working directory, not against the config file
  * unknown config keys are a hard error, so the renderer writes from a closed
    allow-list
  * the embedding API key is never written at all -- slopo reads
    SLOPO_EMBEDDING_API_KEY from the environment ahead of the file, so the
    secret stays out of every file this plugin touches. Enforced in pruneConfig
    rather than at the call site, so no future caller can leak it by passing one.

explainFailure maps slopo's deliberately terse agent-facing messages onto a
message and a next step, and reads the log tail because stderr alone leaves the
agent with no move: on a real failure stderr said "Embedding failed." while the
log named the exact litellm parameter that was rejected.

installEnv redirects UV_TOOL_DIR, UV_TOOL_BIN_DIR and UV_PYTHON_INSTALL_DIR
into $DSH_HOME. Only two directories in this container are persistent mounts;
~/.local, where uv installs by default, is on the container's writable layer
and does not survive a recreation.
This commit is contained in:
dsh-slopo agent committed 2026-09-22 06:11:12 +01:00
1 parent 844f12faa3
commit e956a8c121
3 files changed
+287 -4

No files matched your search

+17 -1
View File
@@ -81,9 +81,21 @@ const NUMERIC_KEYS = new Set([
* @param {Record<string, unknown>} input
* @returns {Record<string, unknown>} only the keys with a usable value
*/
/**
* Keys this plugin will never write into the config file, whatever it is handed.
*
* `embedding_api_key` is a legitimate slopo key, but slopo reads
* `SLOPO_EMBEDDING_API_KEY` from the environment *ahead* of the file, so the
* environment is both higher-precedence and the only place a secret belongs.
* Enforcing that here rather than at the call site means no future caller can
* leak the key into a file on disk by passing it through.
*/
const NEVER_WRITTEN_KEYS = new Set(['embedding_api_key'])
export function pruneConfig(input) {
const output = {}
for (const key of SLOPO_CONFIG_KEYS) {
if (NEVER_WRITTEN_KEYS.has(key)) continue
if (!Object.hasOwn(input, key)) continue
const value = input[key]
if (value === undefined || value === null || value === '') continue
@@ -104,7 +116,11 @@ export function pruneConfig(input) {
export function missingRequired(config) {
return SLOPO_REQUIRED_KEYS.filter((key) => {
const value = config[key]
return value === undefined || value === null || value === ''
if (value === undefined || value === null || value === '') return true
// A dimension of zero is how an unset number arrives from a settings form,
// and slopo requires a positive integer, so it is missing, not configured.
if (key === 'embedding_dimensions') return !Number.isFinite(Number(value)) || Number(value) <= 0
return false
})
}