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
This commit is contained in:
dsh-slopo agent committed 2026-09-22 07:22:03 +01:00
1 parent e758a723e2
commit d7eccc6366
2 files changed
+243 -13

No files matched your search

+70 -13
View File
@@ -33,10 +33,9 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
import Schema from '@deepseek-ai/schemastery'
import { missingRequired, normalizeConfig, renderSlopoConfig } from './lib/config.js'
import { probeEmbeddingEndpoint } from './lib/probe.js'
import {
PLUGIN_NAME,
resolveDshHome,
workspaceKey,
workspaceStateDir,
} from './lib/paths.js'
import {
@@ -423,6 +422,14 @@ export function apply(ctx, config) {
const settings = readSettings() ?? config
const paths = workspacePaths(dshHome, cwd, settings)
const shell = ctx.get('shell')
// Resolved once, up front, so `status` can probe without a second
// resolution. The NO_SANDBOX_POLICY refusal stays with the actions that
// actually execute something.
const sandboxDefault = shell?.sandboxMode
const policyService = sandboxDefault === undefined ? undefined : ctx.get('sandboxPolicy')
const sandboxPolicy = policyService?.resolve(
exec.agent === undefined ? {} : { session: exec.agent.session },
)
const foreground = (value) => ({ kind: 'foreground', action: args.action, configPath: paths.configPath, logPath: paths.logFile, ...value })
@@ -431,8 +438,65 @@ export function apply(ctx, config) {
const problems = []
const slopoConfig = toSlopoConfig(settings, paths, problems)
const missing = missingRequired(slopoConfig)
// Two pre-flight checks, because slopo reports a bad embedding
// configuration as the two words "Embedding failed." on stdout with
// the real reason buried in its agent log. That is a slow way to
// discover a wrong provider prefix or a dimension mismatch.
let slopoVersion
if (binary !== undefined && shell !== undefined) {
try {
const versionResult = await shell.run(
shell.resolve({
command: commandLine([binary, '--version']),
workdir: paths.cwd,
timeoutMs: 30_000,
...(sandboxPolicy === undefined ? {} : { sandboxPolicy }),
}),
)
slopoVersion =
versionResult.exitCode === 0
? (versionResult.stdout?.text ?? '').trim().split('\n')[0]
: `exit code ${versionResult.exitCode}: ${(versionResult.stderr?.text ?? '').trim().slice(0, 200)}`
} catch (error) {
slopoVersion = `could not run: ${error?.message ?? error}`
}
}
// A direct fetch rather than the shell seam: the probe is one small
// HTTP request and does not belong behind a sandbox boundary that
// exists to confine filesystem writes.
const endpoint =
missing.length === 0
? await probeEmbeddingEndpoint({
apiBase: typeof settings.embeddingApiBase === 'string' ? settings.embeddingApiBase : '',
model: String(slopoConfig.embedding_model ?? ''),
dimensions: Number(slopoConfig.embedding_dimensions ?? 0),
apiKey: typeof settings.embeddingApiKey === 'string' ? settings.embeddingApiKey : '',
})
: { checked: false, skipped: 'nothing is configured yet' }
let message
if (missing.length > 0) {
message =
`Not configured yet. Missing: ${missing.join(', ')}. Run this tool with action "configure" ` +
'to set embeddingModel and embeddingDimensions, then "prepare".'
} else if (endpoint.ok === false) {
message =
`Configured, but the embedding backend failed its check: ${endpoint.error}. ` +
`(probed ${endpoint.url} with model "${endpoint.sentModel}")`
} else if (endpoint.checked === true) {
message =
`Ready. ${binary ?? 'slopo'} will use "${slopoConfig.embedding_model}" at ` +
`${Number(slopoConfig.embedding_dimensions)} dimensions, confirmed against ${endpoint.url}.`
} else {
message =
`Configured against "${slopoConfig.embedding_model}" at ` +
`${Number(slopoConfig.embedding_dimensions)} dimensions. Nothing was probed: ${endpoint.skipped}.`
}
return foreground({
ok: missing.length === 0,
ok: missing.length === 0 && endpoint.ok !== false,
exitCode: null,
details: {
dshHome,
@@ -443,19 +507,17 @@ export function apply(ctx, config) {
ignoreFile: existsSync(paths.ignoreFile) ? paths.ignoreFile : null,
agentLog: existsSync(paths.logFile) ? paths.logFile : null,
slopoBinary: binary ?? null,
slopoVersion: slopoVersion ?? null,
slopoInstalledInto: binary === undefined ? null : dshHome,
settingsNamespace: writeSettings === undefined ? null : SETTINGS_NAMESPACE,
embeddingModel: slopoConfig.embedding_model ?? null,
embeddingDimensions: slopoConfig.embedding_dimensions ?? null,
apiKeyConfigured: typeof settings.embeddingApiKey === 'string' && settings.embeddingApiKey !== '',
embeddingEndpoint: endpoint,
missingSettings: missing,
problems,
},
message:
missing.length === 0
? `${binary === undefined ? 'slopo is not installed yet; it will be installed on first use.' : `slopo: ${binary}`}`
: `Not configured yet. Missing: ${missing.join(', ')}. Run this tool with action "configure" ` +
'to set embeddingModel and embeddingDimensions, then "prepare".',
message,
})
}
@@ -505,8 +567,6 @@ export function apply(ctx, config) {
// preset. That confines a call the session asked to run unconfined, and
// on a host with no sandbox runner it fails with SandboxUnavailableError
// before slopo is ever reached.
const sandboxDefault = shell.sandboxMode
const policyService = sandboxDefault === undefined ? undefined : ctx.get('sandboxPolicy')
if (sandboxDefault !== undefined && policyService === undefined) {
return foreground({
ok: false,
@@ -516,9 +576,6 @@ export function apply(ctx, config) {
remedy: 'Compose @deepseek-ai/dsh-sandbox-policy, or mount an unconfining shell executor.',
})
}
const sandboxPolicy = policyService?.resolve(
exec.agent === undefined ? {} : { session: exec.agent.session },
)
// Slopo's state is deliberately outside the workspace, which a
// workspace-write sandbox forbids writing to. Say so before a run and