Commit Graph
9 Commits
Author SHA1 Message Date
dsh-slopo agent 4bb19af2ac Add the settings card — the browser half
The settings namespace was registered but nothing drew a form for it, because
the Plugin configuration tab renders the INTERSECTION of the namespaces the Host
serves and the cards a browser half claims. A served namespace with no card
renders nothing at all. This is the missing card.

Plain ESM in the module-loader wrapper, no build step, requiring only `react` —
which is in the platform seed table. Anything outside that table would have to be
bundled into this file or declared in `dsh.client.external`, and the tests fail
loudly on any other specifier.

It stages edits and writes them on Save; a cleared field is an `unset` rather
than a write of "", so the value re-inherits the composition layer instead of
becoming empty, and the user layer's presence is what marks a field overridden.
Reset clears every override. A namespace this client cannot reach renders
nothing, so a deployment without the Host half shows no trace.

package.json gains an `exports` map (the loader reads `exports["./client"]`; a
bare `main` would not be enough) and the `dsh.client` declaration.
2026-09-22 07:22:03 +01:00
dsh-slopo agent d7eccc6366 Add a real pre-flight to status
Slopo reports a bad embedding configuration as the two words "Embedding failed."
on stdout, with the real reason in its agent log. That is a slow way to learn
that a model string has the wrong provider prefix, or that the dimensions do not
match what the model actually returns — the second of which is expensive, because
slopo freezes the dimensions into the index at first use.

`status` now does three things instead of one: it reports the resolved settings,
it runs `slopo --version` through the shell seam, and it makes one tiny embedding
request against the configured endpoint and compares the returned width with the
configured one. Verified against a real Ollama: a correct config reports ok, a
dimension mismatch names both numbers, and a wrong model returns the endpoint's
own 404 rather than a silent failure minutes into an embed.

The probe is a direct fetch rather than a shell call, because it is one small
HTTP request and does not belong behind a boundary that exists to confine
filesystem writes.

Two smaller things this needed:
- the sandbox policy is now resolved once at the top of execute, so `status` can
  use it without a second resolution
- provider-prefix stripping is an allow-list, not "drop the first segment".
  `unclemusclez/jina-embeddings-v2-base-code:latest` is a real model name with no
  prefix at all, and an unknown first segment is left alone so a wrong guess is
  visible in the reported `sentModel` rather than silent
2026-09-22 07:22:03 +01:00
dsh-slopo agent e758a723e2 Fix: the tool substituted the deployment sandbox default for the session's
dsh-bash-sandbox is the composed ctx.shell, and its resolve() stamps
`request.sandboxPolicy ?? ctx.sandboxPolicy.resolve()`. Omitting the policy
therefore does NOT mean "unconfined" -- it silently substitutes the DEPLOYMENT
default (`DSH_PERMISSION_MODE ?? 'workspace-write'`) for the session's preset.
On a host with no sandbox runner that confines a danger-full-access session and
fails with SandboxUnavailableError before slopo is ever reached, which is
exactly what happened: the tool was unusable while bash worked.

Resolve the calling session's policy the way dsh-tool-bash does, and pass it on
every call. A confining executor with no ctx.sandboxPolicy is now refused
outright rather than guessed at.

That fix exposes a second problem worth naming. Slopo's state lives under
$DSH_HOME, which is outside the workspace root a workspace-write sandbox permits
writing to, so under that preset nothing would work. The tool now refuses before
running and names both ways out -- run under danger-full-access, or set stateDir
to a directory inside the workspace -- instead of letting a write fail halfway
through an embed.

Five tests pin this: the session's policy is resolved exactly once and travels
with the session, a missing policy service is refused, state outside the sandbox
is refused before any execution, state inside it proceeds, and status/configure
never touch the shell at all.
2026-09-22 07:12:03 +01:00
dsh-slopo agent a8e577ae38 Document dsh-slopo, including the traps that cost real time
The provider prefix is not cosmetic: the openai/ prefix makes LiteLLM apply
OpenAI parameter validation to any model behind it, and slopo always passes
dimensions, so a local OpenAI-compatible server needs its own prefix.

Five settings are frozen into the database at first index, so changing the
embedding model means re-embedding everything. And a review cannot bootstrap the
database at all, because agent-review opens the index read-only.
2026-09-22 06:11:17 +01:00
dsh-slopo agent 3373e0b4f6 Test the plugin against the real harness seams and the real CLI
The integration tests load the real plugin module against a thin mock Cordis
context and drive the tool through a real shell into the real slopo binary. The
mock implements only the seams the plugin touches, so a wrong assumption about a
dsh service fails a test instead of producing a plugin that silently does
nothing at runtime.

Two real bugs were caught this way and are now pinned:

  * renderSlopoConfig would write the embedding API key to disk if it were
    handed one. It cannot now.
  * --config-version was placed before the subcommand, which slopo rejects with
    exit code 2. It is declared on the command, not the callback.

The end-to-end test is opt-in (DSH_SLOPO_E2E=1) because it needs a reachable
embedding backend; it indexes a real repository, embeds it, and asserts a real
duplicate cluster comes back.
2026-09-22 06:11:17 +01:00
dsh-slopo agent 31991143e4 Add the plugin host: three skills, a settings namespace, and one tool
The three skills are ports of slopo's own agent configs, published
user-invocable and NOT model-invocable. Slopo makes that choice upstream and the
reason holds here: every one of these commands can spend minutes embedding, so a
model that decides to "check for duplication" mid-task can do it repeatedly.

settings goes through ctx.inject(['settings'], ...) rather than the top-level
inject, so a profile without a settings provider still gets the tool and the
skills and degrades to the composition config. A required dependency would have
suppressed apply entirely -- a silent whole-plugin failure for an optional
capability.

The tool resolves the CLI, materialises the config, and runs the long actions as
background jobs. Background is the right default because embedding takes minutes
and slopo writes progress to a log file rather than stdout, so there is nothing
to stream; the job handle carries the log path instead.

Nothing is defaulted. Guessing an embedding backend would mean guessing where
money is spent, so status says what is missing and configure sets it.
2026-09-22 06:11:17 +01:00
dsh-slopo agent e956a8c121 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.
2026-09-22 06:11:12 +01:00
dsh-slopo agent 844f12faa3 Add path resolution and slopo config rendering
Every path the plugin owns is under $DSH_HOME. Only two directories in the
container this runs in are persistent volumes ($DSH_HOME and the workspace
root); $HOME/.local -- where 'uv tool install' puts slopo and its CPython by
default -- is on the container's writable layer and dies on recreation, not
merely on restart.

renderSlopoConfig writes from a closed allow-list because slopo treats an
unknown config key as a hard error, and quotes every scalar because a path
can contain a colon or a leading dash. Verified against slopo 0.8.0 itself:
it reads the generated file back with no complaint and round-trips all 19
keys. The embedding API key is deliberately absent -- slopo reads
SLOPO_EMBEDDING_API_KEY from the environment ahead of the YAML value, so the
secret never lands on disk.
2026-09-22 06:06:44 +01:00
dsh-slopo agent 3490e5aba0 Scaffold the dsh-slopo bundle and port slopo's three agent skills
The three SKILL.md files are ports of the configs slopo 0.8.0 ships for
Claude Code, not new prose. What changed is the mechanics:

  * name + description frontmatter, both required by dsh
  * disable-model-invocation: true kept verbatim -- dsh has the same key,
    and slopo makes that choice for a reason (every one of these commands
    can spend minutes embedding)
  * allowed-tools / argument-hint / arguments dropped, not dsh keys
  * the !`slopo ...` shell-injection directive replaced with an
    instruction to call the slopo tool plus an explicit failure check,
    because dsh skill bodies do not execute commands

The guidance prose is quoted from slopo; NOTICE.md records the mapping and
the licence obligation that follows from it (AGPL-3.0-or-later, matching
upstream and the LICENSE file fetched from it).
2026-09-22 06:05:28 +01:00