Pular para o conteúdo principal
J

sleep-not-permitted

jjjacksong/sleep-not-permitted

Rejects foreground waiting in DSH agents: waiting commands (sleep, wait, tail -f, watch, ping without -c, infinite loops), blocking wait tools (wait_agent, job_output with wait: true), progress polling (the same check command repeated within a window) and commands that need human input/confirmation (sudo without -n, ssh without BatchMode, login flows, interactive editors, docker -it) are refused with an actionable retry to run in the background or end the turn. Configurable budget and polling window, message language follows the system (zh/en), zero runtime dependencies.

Instalar

dsh plugin --profile web add github:jjjacksong/sleep-not-permitted

README

sleep-not-permitted icon

sleep-not-permitted

中文 · English

A DeepSeek Harness (DSH) plugin that forbids foreground waiting. When an agent tries to run a command whose purpose is waiting (sleep, wait, tail -f, watch, ping without -c, infinite loops), or calls a blocking wait tool (wait_agent, job_output with wait: true), the call is rejected — and the rejection is a set of instructions: run it in the background (run_in_background: true), or end the turn and let the completion notice wake you.

Nothing is silently rewritten: DSH logs and shows tool arguments before dispatch, so a guard can only refuse, not impersonate a different call. The plugin refuses, explains, and gets out of the way.

Why a plugin, and not AGENTS.md or a preset

  • AGENTS.md is per-workspace. You would have to write the rule into every workspace, and every new workspace silently misses it.
  • A preset is per-agent configuration. Every composition needs its own copy, and the user has to pick it.
  • A host plugin is profile-level. Install once and every workspace, session, and subagent is covered. It can carry configuration, be upgraded, disabled, and removed.

Why it is needed

A DSH session moves one step at a time: however long a foreground tool call runs, the session stops with it. bash({ command: "sleep 60" }) looks harmless but freezes the conversation for a minute, and a five-minute wait_agent freezes it for five. DSH has a safety net for slow work (a foreground command that exceeds its timeout is promoted to a background job), but that still burns the timeout first — and waiting is not work at all.

So the plugin does two things:

  1. Prevention — a rule in the system prompt, next to the existing background-jobs section, so the model picks the background form first;
  2. Enforcement — a synchronous ctx.tools.guard() denial when the foreground form is used anyway, with the retry spelled out.

What gets blocked

  • Waiting commands in bash / pwsh / terminal_send: sleep, wait, tail -f, watch, ping without -c, yes, infinite loops, log-follow modes — see the rule table below.
  • Blocking wait tools: wait_agent and job_output with wait: true, when the requested wait exceeds the budget (default 30 seconds).
  • Polling: running the same check command more than pollRepeatThreshold times within pollWindowMs — the "wait a little, check, wait a little, check" substitute for sleeping. It is refused with the same instruction: wrap the waiting itself in a background job.
  • Commands that need human input or confirmation: sudo without -n, ssh without -o BatchMode=yes, login/key-generation flows (npm login, gh auth login, ssh-keygen, …), interactive editors and pagers (vim, less, top, …), docker run/exec -it, read -p, rm/cp/mv -i. They are refused in the foreground and in the background: a background job has no stdin, so it would hang silently — never settling, never notifying.

If what you are waiting for has no DSH job handle (a remote host, an external service), be it a command or a poll: wrap the waiting itself in a background job — for example ssh host 'until [ -f done ]; do sleep 5; done' with run_in_background: true (sleep is allowed inside a background job) — then end the turn; that job's completion wakes the agent. This is the correct replacement for both sleep and polling when there is nothing local to await.

Behavior

  • A foreground wait is rejected with Error: <reason>; the reason contains the exact retry form.
  • A call that already has run_in_background: true passes — that is the shape this plugin wants.
  • Ordinary commands (git status, npm test, curl, grep, …) pass untouched.
  • ask_user_question is not a wait — it is you being asked — and is never blocked.
  • Non-blocking reads (job_output without wait, list_agents, team_task_list) pass.

The default budget (30 s) exists so short coordination waits keep working out of the box (DSH's own Agent Teams guidance has the Lead wait for teammates). Waits above the budget — the five-minute wait_agent(timeout_ms: 300000) case — are rejected. Set maxBlockingWaitMs: 0 for strict mode: not a single second of foreground waiting.

After a background job finishes

A completion notice means the job finished, not that it succeeded. The notice carries the status (completed / failed / killed) plus the label; read the output and exit code with job_output before continuing, and diagnose a failed/killed job instead of retrying blindly.

Background jobs have no timeout of their own. A job that dies mid-flight or hangs while waiting for something that never happens never settles — and so never notifies. Bound it yourself: wrap the command in timeout N, keep a deadline in retry loops, or write a marker/report file at the end. For cross-machine waits the same applies to the remote wait loop: give it a maximum wait and let it exit with a failure marker on timeout, so the local job always settles and always notifies.

If the work needs a human decision in the middle, do not background it: confirm first (ask_user_question, or the DSH approval channel), then start it with non-interactive flags.

Rules

idmatches
sleepsleep N in command position (sudo sleep, timeout 30 sleep, do sleep, && sleep, …)
sleep-nested-shellnested shell payloads such as bash -c "sleep 10"
sleep-scripttime.sleep(30), python -c "...sleep(5)"
waitthe shell builtin wait
tail-followtail -f / tail -F / tail --follow
watchwatch <cmd> (re-runs forever)
ping-foreverping without -c
yesbare yes (endless output)
infinite-loopwhile true / while : / until false
follow-logsdocker/podman logs -f, journalctl -f, kubectl logs -f

Plain text inside quotes does not count (echo "sleep 30", grep 'sleep 30' file pass); in multi-line scripts, command position is evaluated per line.

Configuration

Override the bundle patch row from the profile's cordis.patch.yml, or edit the plugin's cordis.patch.yml:

- id: sleep-not-permitted
  name: 'dsh-plugin-sleep-not-permitted'
  config:
    enabled: true                 # false = turn the whole guard off
    tools: [bash, pwsh, terminal_send]
    blockingTools: [wait_agent]   # blocking wait tools: reject above the budget
    blockingWaitFlagTools: [job_output]  # reject when wait: true and above the budget
    maxBlockingWaitMs: 30000      # 0 = reject every foreground wait
    antiPolling: true             # refuse repeated check commands (busy-poll)
    pollRepeatThreshold: 3        # max times one command may appear inside the window
    pollWindowMs: 120000          # polling window, milliseconds
    interactiveGuard: true        # refuse commands that wait for input/confirmation
    disabledInteractiveRules: []  # switch off individual interactive rule ids
    language: auto                # auto | both | zh | en (message + prompt language)
    announceInPrompt: true        # false = enforcement only, no prompt rule
    disabledRules: []             # switch off individual rule ids from the table
    extraRules:                   # add your own rules; pattern is a regex string
      - id: long-curl
        pattern: 'curl\s+[^|;&]*--max-time\s+\d{3,}'
        label: long-timeout curl
        reason: This curl can hang for a long time — run it in the background.

Invalid configuration fails loud at mount time (unknown rule id, empty tools, invalid regex, non-boolean switch, unknown language) — there is no "installed but silently inactive" state.

Message language

language: auto (the default) follows the user's system language: it checks DSH_LOCALE, LC_ALL / LC_MESSAGES / LANG, then the macOS system language preference, then Node's Intl default, and falls back to English. A Chinese system gets Chinese-only messages and a Chinese-only prompt section (roughly half the prompt cost); both emits both languages; zh / en pin one explicitly. Resolution happens once at mount.

Install / uninstall

The package is a standard DSH bundle (dsh.bundle.patch → cordis.patch.yml).

git clone https://github.com/JJJACKsong/sleep-not-permitted.git

# desktop profile (managed by the Electron app): use plugin_manager install_bundle
#   with the absolute path of the cloned directory
# other profiles:
dsh plugin --profile web add /path/to/sleep-not-permitted

Prebuilt tarball (no clone, no build):

dsh plugin --profile web add https://github.com/JJJACKsong/sleep-not-permitted/releases/download/v0.1.0/dsh-plugin-sleep-not-permitted-0.1.0.tgz

Uninstall:

dsh plugin --profile web remove dsh-plugin-sleep-not-permitted
# desktop: plugin_manager remove_bundle dsh-plugin-sleep-not-permitted

Disable without uninstalling: set enabled: false, or toggle the sleep-not-permitted row in the plugin manager.

Tests

node --test test/detect.test.mjs test/plugin.test.mjs
  • test/detect.test.mjs — detector positives and negatives (command position, quotes, nested payloads, false-positive protection);
  • test/plugin.test.mjs — a minimal ctx double checks deny/pass, defaults, strict mode, message language, and fail-loud configuration.

Verify it works

  • Foreground sleep 5 → rejected immediately with Error: …;
  • wait_agent(timeout_ms: 300000) → rejected (budget 30 s);
  • Background sleep 5 (run_in_background: true) → returns a job id at once, notify on completion;
  • The system prompt contains the rule block (announceInPrompt: true).

Compatibility

Tested against DSH 0.1.5-rc.2 and 0.2.1-rc.3. It relies on two host seams — ctx.tools.guard() and ctx.systemPrompt.section() — and fails loud at mount if they change.

Boundaries

  • Does not guess "might be slow". npm install, cargo build and other commands that make progress still run in the foreground; the tool timeout promotes them to a background job (default 120 s, capped at 600 s).
  • Does not block work. Long synchronous tasks such as workflow or deliver_gate are not waits and are left alone.
  • Does not rewrite arguments. A denied call costs one retry round trip; the system-prompt rule is there to keep that rare.
  • Does not inspect script files. How long bash script.sh sleeps inside cannot be judged statically; the tool timeout is the backstop.

Plugins relacionados