diff --git a/.agents/skills/verify-deepseekbot/features/approval-templates.md b/.agents/skills/verify-deepseekbot/features/approval-templates.md index 02861784b..f64035624 100644 --- a/.agents/skills/verify-deepseekbot/features/approval-templates.md +++ b/.agents/skills/verify-deepseekbot/features/approval-templates.md @@ -5,6 +5,7 @@ A Bot inherits the optional global default, selects one complete shared template ## Sub-features - `library`: Bot settings → Auto approval → Global default template (DSH Select), existing template library and Reviewer model. No global default means inherited review is off. Read failures remain errors. +- `editor`: New template requires Blank or one selected source; edit independent Always ask/Always allow rows in the same grouped card list as External identities/connectors (outer top/bottom corners, straight middle joins), plus free text; save alone or apply to a named Bot. Copy a custom or built-in template independently; shared-original editing lists actual users including global inheritors. Read back full content and selection through CLI. - `cli`: `approval-template list --bot `, `approval-template-global-set `, `approval-template-set `; library create/edit commands retain existing Markdown contents. - `select`: Channel settings → Auto approval, or composer Workspace Grants panel → Auto approval, uses the same DSH Select. Inheritance shows its effective global source. Specific selection replaces global; Off retains deterministic rules. - `mobile`: Bot profile → Settings → Permissions → Workspace grants → Auto approval template, inside the existing iOS sheet / Android `ModalBottomSheet`. Inherit global shows the resolved global template (or Off); selecting Off then Inherit global writes null then @global through the same Host owner. Legacy Hosts without a global-selection field retain their supported choices. @@ -33,7 +34,7 @@ Use the shared device QA lane and T3 device tools. The same synthetic Bot and is ## Gotchas - `/tmp` may already be writable; an ordinary file write there need not reach an approval gate. -- Three-part editing, card-rule table and confirmed bulk deletion fallback are separate issues; this journey does not qualify them. +- Card-rule table and confirmed bulk deletion fallback remain separate issues. For the editor, verify legacy Markdown survives conversion and an edit during a Review changes only the next captured version. - Browser screenshots establish Web behavior only. Preserve failed drives and observe before retrying. ## Human result retention (#4211) diff --git a/changes/4244-template-guidance-editor.md b/changes/4244-template-guidance-editor.md new file mode 100644 index 000000000..d3c9bce20 --- /dev/null +++ b/changes/4244-template-guidance-editor.md @@ -0,0 +1,11 @@ +--- +section: Changed +--- + +## en + +- Added explicit blank or selected-source template creation, independent copies, three-part Reviewer guidance, and separate save/apply choices; shared-original edits disclose the Bots using them, and Always ask/Always allow use the same grouped card list as external identities and connectors ([#4244](https://github.com/BotHarness/DeepSeekBot/issues/4244)). + +## zh + +- 新增明确选择空白或来源模板创建独立副本、三部分 Reviewer 指导及仅保存/保存并应用选项;编辑共享原件时显示使用它的 Bot;Always ask/Always allow 使用与外部身份及连接器一致的分组卡片列表([#4244](https://github.com/BotHarness/DeepSeekBot/issues/4244))。 diff --git a/docs/adr/0342-bot-approval-selects-one-template-and-keeps-deterministic-authorization-separate.md b/docs/adr/0342-bot-approval-selects-one-template-and-keeps-deterministic-authorization-separate.md index af51461ff..02b637b66 100644 --- a/docs/adr/0342-bot-approval-selects-one-template-and-keeps-deterministic-authorization-separate.md +++ b/docs/adr/0342-bot-approval-selects-one-template-and-keeps-deterministic-authorization-separate.md @@ -29,3 +29,7 @@ The existing application-defined approval owner remains responsible for decision This amends ADR-0207's template selection, Markdown-only policy shape, three Reviewer outcomes and rejection circuit breaker as target design, not retroactive runtime fact. ADR-0249 continues to govern explicit management authorization. The existing issue bodies are not rewritten by this record. Rejected alternatives were global-plus-Bot layering (ambiguous effective policy), applying new defaults to old disabled Bots (implicit opt-in), autonomous model rejection (removes the Human's decision on unapproved requests), hidden copies/application (unclear scope), and refusing deletion until every reference is removed (unnecessary repeated work). The selected design makes defaults dynamic and deletion easier, at the cost of a carefully disclosed bulk change and immutable policy snapshots for already-started Reviews. + +## File compatibility for #4244 + +Three-part editing keeps the existing template-file owner and 32 KiB bounded reader. A saved structured template uses the exact marker ``, a newline, then JSON containing `alwaysAsk` and `alwaysAllow` string arrays and `freeText`. This is a versioned content representation inside the existing file, not a SQLite migration. Legacy Markdown without that marker maps in full to free text with empty row arrays; headings are not parsed into rules or rewritten. Invalid marked content is unavailable, so a Review falls back to the Human. The Review content hash covers the complete saved representation; decoding for Reviewer assembly uses only that captured text. Copies contain content only. Global defaults and built-ins are unchanged. diff --git a/docs/architecture/bot-approval-policy.md b/docs/architecture/bot-approval-policy.md index bfb2a575f..c55a80068 100644 --- a/docs/architecture/bot-approval-policy.md +++ b/docs/architecture/bot-approval-policy.md @@ -110,6 +110,8 @@ Future delivery must demonstrate inherited versus specific/Off behavior, Reviewe Performance assessment: this change edits documentation only, introduces no dependency and changes no runtime hot path, so no runtime benchmark is warranted. Future E2/E3 work must separately budget added admission work and model calls, prove deterministic hits make no Reviewer call, and verify the affected real journey under the repository's performance policy. +The #4244 editor retains the template-file owner and 32 KiB bounded reads. ADR-0342 records the v1 content representation; legacy Markdown maps intact to free text. Copies contain only content. Save and apply are separate choices, shared-original editing shows global inheritors among actual users, and content edits affect future Reviews. + ## Human approval result presentation A Reviewer handoff appends an explicit `ask-human` result to the canonical Channel before the Human request, with its captured template identity/version, sanitized input and reason. Record failure still routes to Human; cancellation and owner changes cannot grant authorization. Historical autonomous rejection records remain readable; the historical rejection override cannot accept an `ask-human` record. diff --git a/docs/architecture/bot-approval-policy.zh.md b/docs/architecture/bot-approval-policy.zh.md index fe3a4b290..988f44fde 100644 --- a/docs/architecture/bot-approval-policy.zh.md +++ b/docs/architecture/bot-approval-policy.zh.md @@ -112,6 +112,8 @@ Human 在 Q1–Q18 分轮访谈后确认了完整汇总;原设计树无未决 性能评估:本次只有文档变更,无新依赖、无运行热路径变化,因此免运行性能基准。未来 E2/E3 须分别预算新增准入工作及模型调用,证明确定性命中不调用 Reviewer,并依仓库性能规则验证实际旅程。 +#4244 的三部分编辑沿用模板文件 owner 与 32 KiB 有界读取;使用 ADR-0342 记录的 v1 内容表示,旧 Markdown 完整映射为补充说明。复制只复制内容;保存与应用分开,编辑共享原件显示全局继承者在内的使用者。编辑仅影响后续 Review。 + ## Human 审批结果展示 Reviewer 转交时,先向同一 canonical Channel 写入显式 `ask-human` 结果,再创建 Human 请求,保留 Review 起始模板身份、版本、已脱敏输入和原因。记录失败仍转 Human;取消或 owner 改变不能授权。历史自动拒绝记录继续可读,旧拒绝覆盖入口不能接受新的 `ask-human` 记录。 diff --git a/docs/bot-cli.md b/docs/bot-cli.md index ade4b5cc3..2e9b9219a 100644 --- a/docs/bot-cli.md +++ b/docs/bot-cli.md @@ -2107,3 +2107,21 @@ deepseekbot models allowed [--as ] ``` Tool effect declarations (E1) preserve the existing tool commands and approval outcomes. Full Access, saved rules, Secrets, device targets and Workspace Grants retain their existing checks; no new route or CLI authority is introduced. + +### Three-part approval guidance + +Create an empty draft explicitly with `deepseekbot approval-template create my-draft --blank`, or an independent copy with `deepseekbot approval-template create my-copy --from cautious`. No source is selected implicitly; copies retain no origin link. Existing `--file` / `--text-stdin` Markdown input remains supported. + +For three-part editing, write a UTF-8 JSON file: + +```json +{ + "alwaysAsk": ["Send messages or spend money"], + "alwaysAllow": ["Read local project files"], + "freeText": "Ask me when unsure." +} +``` + +Run `deepseekbot approval-template edit my-draft --parts-file guidance.json`. Read it back with `deepseekbot approval-template show my-draft`; `text` contains the versioned representation. Add `--apply-to ` to create/edit to save and then select it for that Bot. Without that flag, saving changes no selection. Applying uses the existing Bot selection authorization; if applying fails, the saved draft remains and can be selected separately. + +Editing a shared original affects future Reviews for every Bot in its `usedBy` list, including global inheritors. Copy first to work independently. Always ask has priority over Always allow, then free text supplements both. These are fallible natural-language instructions, never deterministic Tool Approval Rules. Conflicts, missing coverage or uncertainty ask the Human. Legacy Markdown remains intact as free text when converted; already-started Reviews retain their captured content/version. diff --git a/docs/bot-cli.zh.md b/docs/bot-cli.zh.md index d4b2f10fe..18e01c11d 100644 --- a/docs/bot-cli.zh.md +++ b/docs/bot-cli.zh.md @@ -2048,3 +2048,21 @@ Web 手动检查请求最多等待 25 秒,关闭自检卡片会取消请求。 公网阶段仅对 DNS、网络、超时及 HTTP 429/5xx 短暂失败重试:最多四次,间隔 1/3/6 秒,仍受同一个 20 秒总时限约束。证书或身份失败不重试。每次自检完成后,界面刷新一次账号电脑列表。 工具效果声明(E1)保持既有工具命令与审批结果。Full Access、保存规则、Secrets、设备目标和 Workspace Grant 仍使用原检查;不新增 route 或 CLI 权限。 + +### 三部分审批指导 + +使用 `deepseekbot approval-template create my-draft --blank` 明确创建空白草稿,或使用 `deepseekbot approval-template create my-copy --from cautious` 创建独立副本。不会自动选取来源,副本不保留来源关联。仍支持 `--file`/`--text-stdin` Markdown 输入。 + +三部分编辑使用 UTF-8 JSON 文件: + +```json +{ + "alwaysAsk": ["发送消息或花钱"], + "alwaysAllow": ["读取本地项目文件"], + "freeText": "不确定时问我。" +} +``` + +运行 `deepseekbot approval-template edit my-draft --parts-file guidance.json`,再用 `deepseekbot approval-template show my-draft` 读回;`text` 包含带格式版本的内容。创建/编辑时添加 `--apply-to ` 才会在保存后为该 Bot 选用模板。不加时不改变选择。应用仍通过现有 Bot 选择授权;应用失败时已保存的草稿保留,可单独选用。 + +编辑共享原件影响 `usedBy` 中所有 Bot 的后续 Review,包括全局继承者。需要独立修改时先复制。总是询问优先于总是允许,补充说明不能取消前者。这些是可能被模型误解的自然语言指导,不是确定性 Tool Approval Rules;冲突、未覆盖或不确定均交给 Human。转换时旧 Markdown 完整保留为补充说明,已经开始的 Review 保留捕获的内容和版本。 diff --git a/packages/client/src/client/approval-template-library.tsx b/packages/client/src/client/approval-template-library.tsx index 7719b524e..ae3466c02 100644 --- a/packages/client/src/client/approval-template-library.tsx +++ b/packages/client/src/client/approval-template-library.tsx @@ -1,6 +1,11 @@ -import { useClientFields } from './use-client-store.js'; +import { + decodeApprovalTemplateGuidance, + encodeApprovalTemplateGuidance, + type ApprovalTemplateGuidance, +} from '../../../core/src/workspaces/approval-template-guidance.js'; +import { useClientFields, useStoreSlice } from './use-client-store.js'; import { useState, type ReactElement } from 'react'; -import { Button, Tag } from '@deepseek-ai/dsh-client-ui-primitives'; +import { Button, Input, Tag } from '@deepseek-ai/dsh-client-ui-primitives'; import * as stylex from '@stylexjs/stylex'; import { templateDescription, templateTitle } from './approval-template-entry.js'; @@ -10,21 +15,32 @@ import { errorMessage, loadApprovalTemplates, setGlobalApprovalTemplate, + setBotApprovalTemplate, updateApprovalTemplate, type ApprovalTemplateView, type BridgeCall, } from './bridge.js'; import { Callout } from './ds/callout.js'; -import { FieldStack, TextAreaField, TextField } from './ds/field.js'; +import { Field, FieldStack, TextAreaField, TextField } from './ds/field.js'; import { ConfirmDialog, DangerTextButton, FormDialog } from './ds/form-dialog.js'; import { Note } from './ds/note.js'; import { Select } from './ds/select.js'; import { SettingsRow, SettingsSection } from './ds/settings-row.js'; -import { space } from './ds/tokens.stylex.js'; +import { border, color, space, text } from './ds/tokens.stylex.js'; +import { ChannelSidebarIcon } from './channel-sidebar-icon.js'; +import { SidebarCardList, SidebarCardRow } from './sidebar-card.js'; import type { BotHarnessTranslate } from './locale.js'; import { LoadingSkeleton } from './loading-skeleton.js'; import { useMountedResource } from './mounted-resource.js'; -import type { ClientStore } from './store.js'; +import type { ClientState, ClientStore } from './store.js'; + +function currentTemplateBot(state: ClientState): string | undefined { + const selection = state.selection; + if (selection?.kind === 'bot') return selection.slug; + if (selection?.kind === 'channel') + return state.channels.find((channel) => channel.id === selection.channelId)?.botSlug; + return undefined; +} const TEMPLATE_LIBRARY_FIELDS = ['bots'] as const; @@ -32,9 +48,23 @@ const NAME = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/u; const styles = stylex.create({ meta: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: space.x2 }, + ruleInput: { + boxSizing: 'border-box', + width: '100%', + minWidth: 0, + borderWidth: 0, + borderRadius: 0, + backgroundColor: 'transparent', + fontWeight: text.regular, + outlineStyle: 'solid', + outlineWidth: border.focus, + outlineColor: { default: 'transparent', ':focus-within': color.fg }, + outlineOffset: -2, + }, }); type Editor = + | { mode: 'source' } | { mode: 'create'; name: string; text: string } | { mode: 'view'; template: ApprovalTemplateView } | { mode: 'edit'; template: ApprovalTemplateView; text: string }; @@ -52,9 +82,89 @@ function copyName(name: string, taken: readonly ApprovalTemplateView[]): string return candidate; } -function copyText(text: string, suffix: string): string { - return text.replace(/^(#[ \t]+)(.+)$/mu, (_match, mark: string, title: string) => - title.trimEnd().endsWith(suffix) ? mark + title : mark + title.trimEnd() + ' ' + suffix, +function GuidanceFields({ + text, + onChange, + t, +}: { + text: string; + onChange(text: string): void; + t: BotHarnessTranslate; +}): ReactElement { + const parts = decodeApprovalTemplateGuidance(text); + const update = (next: ApprovalTemplateGuidance): void => + onChange(encodeApprovalTemplateGuidance(next)); + return ( + + {t('autoApproval.library.guidanceHint')} + {(['alwaysAsk', 'alwaysAllow'] as const).map((key) => { + const label = t( + key === 'alwaysAsk' + ? 'autoApproval.library.alwaysAsk' + : 'autoApproval.library.alwaysAllow', + ); + return ( + + {() => ( + + {parts[key].map((row, index) => { + const rowLabel = label + ' ' + String(index + 1); + return ( + {index + 1}} + title={ + + update({ + ...parts, + [key]: parts[key].map((item, position) => + position === index ? event.currentTarget.value : item, + ), + }) + } + /> + } + trailing={ + + } + /> + ); + })} + update({ ...parts, [key]: [...parts[key], ''] })} + /> + + )} + + ); + })} + update({ ...parts, freeText })} + /> + ); } @@ -73,9 +183,12 @@ export function ApprovalTemplateLibrarySettings({ const [loadError, setLoadError] = useState(); const [editor, setEditor] = useState(); const [deleting, setDeleting] = useState(); + const [applyTo, setApplyTo] = useState(''); const [busy, setBusy] = useState(false); const [error, setError] = useState(); - useClientFields(TEMPLATE_LIBRARY_FIELDS, store); + const { bots } = useClientFields(TEMPLATE_LIBRARY_FIELDS, store); + const currentBotSlug = useStoreSlice(store, currentTemplateBot); + const currentBot = bots.find((bot) => bot.slug === currentBotSlug); const reload = async (): Promise => { const next = await loadApprovalTemplates(call); @@ -121,9 +234,15 @@ export function ApprovalTemplateLibrarySettings({ const close = (): void => { setEditor(undefined); + setApplyTo(''); setDeleting(undefined); setError(undefined); }; + const createDraft = (draft: Extract): void => { + setApplyTo(''); + setError(undefined); + setEditor(draft); + }; const run = (operation: () => Promise): void => { if (busy) return; setBusy(true); @@ -165,6 +284,63 @@ export function ApprovalTemplateLibrarySettings({ ); } if (editor === undefined) return null; + if (editor.mode === 'source') + return ( + + + ); + const apply = async (name: string): Promise => { + if (applyTo !== '') await setBotApprovalTemplate(call, applyTo, name); + }; if (editor.mode === 'create') { const nameProblem = editor.name !== '' && !NAME.test(editor.name) @@ -183,7 +359,13 @@ export function ApprovalTemplateLibrarySettings({ error={error} onCancel={close} onSubmit={() => - run(() => createApprovalTemplate(call, editor.name, { text: editor.text })) + run(async () => { + const template = await createApprovalTemplate(call, editor.name, { + text: editor.text, + }); + setEditor({ mode: 'edit', template, text: editor.text }); + await apply(editor.name); + }) } > @@ -198,13 +380,12 @@ export function ApprovalTemplateLibrarySettings({ autoComplete="off" onChange={(name) => setEditor({ ...editor, name: name.trim() })} /> - setEditor({ ...editor, text })} + t={t} /> + {saveChoice} ); @@ -222,10 +403,10 @@ export function ApprovalTemplateLibrarySettings({ submitData={{ 'data-approval-template-submit': 'copy' }} onCancel={close} onSubmit={() => - setEditor({ + createDraft({ mode: 'create', name: copyName(template.name, list), - text: copyText(template.text ?? '', t('autoApproval.library.copySuffix')), + text: template.text ?? '', }) } > @@ -248,34 +429,56 @@ export function ApprovalTemplateLibrarySettings({ labels={labels} submitLabel={t('autoApproval.library.save')} submitData={{ 'data-approval-template-submit': 'save' }} - submitDisabled={editor.text.trim() === '' || editor.text === template.text} + submitDisabled={ + editor.text.trim() === '' || (editor.text === template.text && applyTo === '') + } busy={busy} error={error} onCancel={close} - onSubmit={() => run(() => updateApprovalTemplate(call, template.name, editor.text))} + onSubmit={() => + run(async () => { + if (editor.text !== template.text) + await updateApprovalTemplate(call, template.name, editor.text); + await apply(template.name); + }) + } secondary={ - { - setError(undefined); - setDeleting(template); - }} - > - {t('autoApproval.library.delete')} - + <> + + { + setError(undefined); + setDeleting(template); + }} + > + {t('autoApproval.library.delete')} + + } > {names === undefined ? null : ( {t('autoApproval.library.inUse', { names })} )} - setEditor({ ...editor, text })} + t={t} /> + {saveChoice} ); })(); @@ -348,7 +551,7 @@ export function ApprovalTemplateLibrarySettings({ variant="outline" size="sm" disabled={templates === undefined} - onClick={() => setEditor({ mode: 'create', name: '', text: '' })} + onClick={() => setEditor({ mode: 'source' })} > {t('autoApproval.library.create')} @@ -391,6 +594,15 @@ export function ApprovalTemplateLibrarySettings({