fix(daemon): state the skill read-only boundary instead of letting agents guess (#7720)

* fix(daemon): state the skill read-only boundary instead of letting agents guess

A user asked the agent to update one of his own skills. Every write to the
skill directory came back `Operation not permitted` — by design:
`resolveChatExtraAllowedDirs` keeps skill roots out of the writable
allowlist, and Codex is given no skill directory at all because it treats
`--add-dir` as write access (#622).

Nothing in the composed system prompt said so. With no stated boundary and
no stated exit, the agent invented one: it told the user to open Open
Design's settings and add the directory to a "文件系统 / 工作区 / 可写目录 /
writable roots" option. No such setting exists — not for writable roots, not
for a sandbox mode, not for an approval policy. The user went looking for
it, could not find it, and filed a diagnostics bundle (0.21.0, packaged
stable).

Adds an `## Editing skills` section that states the boundary and its
symptom, names the real edit surface (the Integration view's Skills tab —
NOT a "Settings > Skills" page, which does not exist), and forbids inventing
a settings path to explain the failure. The missing settings are worded as a
statement about the current build rather than a refusal, so contributors do
not read it as "we will never build this"; a controlled skill-write path is
still on the table.

Gated on `executionProfile === 'filesystem'`. Deliberately NOT gated on
`isSlimCore` — slim is the production default, so that gate would ship this
to nobody — nor on an active skill, since "update my skill" arrives on turns
where no skill is bound. Ask mode and API/BYOK mode have no file tools and
so cannot hit the boundary; the matrix snapshot pins that (18 of 20
scenarios receive it, `ask-mode-full-context` and `api-mode-byok` do not).

Red spec: tests/skill-write-guidance.test.ts, 5/5 red on this branch with
the `parts.push` removed, and 1/5 red (the wording guard alone) when
"does not currently expose" is rewritten to an absolute "we do not support".
Assertions are scoped to the extracted section rather than the whole prompt:
an earlier draft asserted /today/i document-wide and passed with no
implementation at all.

Note: like any prompt change this shifts the stable-prompt hash once, so
existing conversations take a single `stable-prompt-changed` cache miss on
first turn after upgrade.

Tested: pnpm --filter @open-design/daemon exec vitest run tests/skill-write-guidance.test.ts tests/system-prompt-template.test.ts tests/prompt-templates.test.ts tests/intent-signal-stable-prompt-cache.test.ts tests/official-system-prompt.test.ts tests/prompt-telemetry.test.ts tests/prompts tests/chat-route.test.ts (20 files, 355 tests)
Tested: pnpm --filter @open-design/daemon build
Tested: pnpm guard
Tested: pnpm typecheck
Not-tested: live packaged client — prompt-only change, no runtime surface

* fix(daemon): name the staged .od-skills copy as a non-persisting write

The boundary section told agents that writing a skill returns `Operation
not permitted`. That is true for the real skill root, and false for the
one skill path the prompt stack actually advertises:
`withSkillRootPreamble` names `<cwd>/.od-skills/<folder>/` as the primary
**Skill root**, and it sits inside the agent's writable project cwd. A
`Write`/`Edit`/`Bash` against `.od-skills/<id>/SKILL.md` therefore
succeeds, reads as an install, and is discarded when `stageActiveSkill`
re-stages the copy on the next turn — the same misdirection the section
was written to stop, minus the error that would have stopped it.

The handover line made that landing spot likely rather than incidental:
"write the proposed `SKILL.md` into the project folder" pointed at a
folder that already contains a directory labelled the skill root.

So: name the staged copy and say a write there does not persist, and
send the proposal to a new `.md` file in the project folder, explicitly
not under `.od-skills/`. The two new assertions are section-scoped and
go red against the previous wording — `/operation not permitted/` could
not, because on this path nothing is denied.

* fix(od-next): state the same skill read-only boundary on the strategy path

`server.ts` picks one composer per run:

    const prompt = odNextStrategyRecipe
      ? composeOdNextStrategyCorePromptV2(odNextStrategyRecipe)
      : composeSystemPrompt(systemPromptInputs);

so everything the previous commit added lives on the classic branch only.
When the OD Next strategy is assigned, `composeSystemPrompt` is never
called and there is no always-resident block underneath — the boundary
section disappears wholesale, and the reported failure comes back.

OD Next is not a path where this is hypothetical. It is the path with the
sharper version of the same trap: `materializeFrozenSkillPackage` writes
the frozen Skill package into `<cwd>/.od-skills/`, and the strategy prompt
hands the agent those exact roots — `materializedRoot` in the Skill roster
(`renderFrozenSkillRosterContext`) and `Frozen side-file root:` in each
Skill body (`resolveFrozenSkillBundleBodies`). Writable directory,
advertised path, no stated boundary, and a write there additionally breaks
the frozen identity the daemon verifies.

So the same four statements land in the core strategy prompt's
`## Agent and runtime boundaries`: the boundary, the materialized roots
that accept a write without persisting it, the real Skills-tab exit, and
the prohibition on inventing a sandbox / writable-roots / approval-policy
setting.

`assets.core.version` moves 2.1.0 -> 2.2.0 with the text. Package identity
is content-hashed from these bytes at apply time
(`buildStrategyPackageIdentity`), and `plugins-strategy-recipe.test.ts`
copies the real bundled folder, so that suite exercises identity against
the edited asset rather than a fixture.

The new assertions are scoped to the section (`.od-skills` also appears in
the task-profile assets, so a document-wide match could not go red) and
whitespace-normalized, since the asset is hard-wrapped prose. Five of the
six go red against the previous content; the sixth is labelled in-file as a
locator check that stays green on `main` by design.
This commit is contained in:
lefarcen
2026-09-06 11:02:40 +00:00
committed by GitHub
parent 3d0d15fc55
commit c5ae6292c4
6 changed files with 324 additions and 19 deletions
@@ -159,6 +159,27 @@ formats follow the general orchestration Skill.
- Open Design owns session creation, continuation, and expiry. Handle only the
current request and the continuation instructions you receive; never manage
sessions yourself.
- Skill directories are not writable through your file tools. A skill is part
of your own instructions, so it is never edited as a side effect of a task;
a write attempt returns `Operation not permitted`, and retrying, changing
the path, or routing the same write through a shell command will not help.
- The `.od-skills/` roots this strategy names — every `materializedRoot` in
the Skill roster and every `Frozen side-file root` in a Skill body — are
read-only materialized copies inside the project. They sit in a directory
you can write, so an edit there will appear to succeed while updating
nothing, and it breaks the frozen Skill identity Open Design verifies. Read
them; never write them, and never report a successful write there as having
created or updated a Skill.
- When the user asks you to create or change a Skill, do the work and hand it
over instead of installing it yourself: write the proposal as a new `.md`
file in the project folder — never under `.od-skills/` — and tell the user
to paste it in through the Integration view's Skills tab, which is where
Skills are edited in the app.
- Open Design does not currently expose a sandbox mode, a writable-directory
or "writable roots" list, or an approval-policy setting. Do not tell the
user to look for one, and do not invent a settings path, menu, or option
name to explain the failure — they will go looking and find nothing. State
the limitation plainly and point at the Skills tab instead.
## Design baseline
@@ -37,7 +37,7 @@
"assets": {
"core": {
"path": "./assets/core-system-prompt.md",
"version": "2.1.0"
"version": "2.2.0"
},
"orchestration": {
"path": "./assets/general-orchestration.md",