dsh-embedded-workbench 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en-US.md +4 -1
- package/README.md +4 -1
- package/lib/client.js +214 -0
- package/lib/index.js +46 -4
- package/lib/types/index.d.ts +8 -2
- package/package.json +46 -31
- package/src/client.js +214 -0
- package/src/index.ts +60 -5
package/README.en-US.md
CHANGED
|
@@ -208,7 +208,8 @@ Skills are invoked with `$skill-name`. ZCode also auto-discovers from `.claude/s
|
|
|
208
208
|
## Requirements
|
|
209
209
|
|
|
210
210
|
- Claude Code v2.1+ / Codex CLI latest / Cursor 2.5+ / Kimi CLI latest / OpenCode latest / ZCode 3.0+
|
|
211
|
-
- DeepSeek Harness (dsh): dev preview —
|
|
211
|
+
- DeepSeek Harness (dsh): dev preview — supports `>= 0.1.0-rc.7` (the standing declaration; this round re-measured 0.1.5-rc.2 / 0.1.5-rc.3 / 0.1.6-alpha.2 / 0.1.7-alpha.1 / 0.1.7-alpha.2 / 0.1.7-rc.1 / 0.1.7-rc.2 / 0.2.0-rc.1 / 0.2.0-rc.2 — per-release evidence in [DSH-COMPATIBILITY.md](DSH-COMPATIBILITY.md))
|
|
212
|
+
- The Web Plugins-page "Gate injection" switch requires **dsh ≥ 0.1.7-alpha.1** — its settings service must project live fields. On older dsh the plugin and its 8 skills still load and still inject, with the switch simply absent and **no error**: below schemastery 3.18.3 the field degrades to an ordinary boolean, and a settings service without `whileServed` makes the client half register nothing.
|
|
212
213
|
- No external dependencies
|
|
213
214
|
|
|
214
215
|
## Configuration
|
|
@@ -220,6 +221,8 @@ In DeepSeek Harness, the bundle accepts a small configuration object:
|
|
|
220
221
|
| `enabled` | boolean | `true` | Set to `false` to drop the first-step gate injection entirely; skill registration is unaffected. |
|
|
221
222
|
| `gateContent` | string | built-in gate text | Override the text injected into the first model step. |
|
|
222
223
|
|
|
224
|
+
The switch is editable live in the dsh Web GUI: sidebar **Plugins** → this plugin's card → "Gate injection". It takes effect without a profile restart and controls only the injected text — turning it off leaves all eight skills registered. The same card also carries a coarser row switch: turning that off unmounts the whole row (the skills and this switch go with it). Persistent overrides still go through the profile patch below.
|
|
225
|
+
|
|
223
226
|
To change it, override the row by id in your profile's `cordis.patch.yml` (the example below customises the gate text):
|
|
224
227
|
|
|
225
228
|
```yaml
|
package/README.md
CHANGED
|
@@ -205,7 +205,8 @@ cp -r embedded-workbench/skills/* .zcode/skills/
|
|
|
205
205
|
## 依赖
|
|
206
206
|
|
|
207
207
|
- Claude Code v2.1+ / Codex CLI 最新版 / Cursor 2.5+ / Kimi CLI 最新版 / OpenCode 最新版 / ZCode 3.0+
|
|
208
|
-
- DeepSeek Harness (dsh): dev preview —
|
|
208
|
+
- DeepSeek Harness (dsh): dev preview — 支持 `>= 0.1.0-rc.7`(沿用既有声明;本轮实测覆盖 0.1.5-rc.2 / 0.1.5-rc.3 / 0.1.6-alpha.2 / 0.1.7-alpha.1 / 0.1.7-alpha.2 / 0.1.7-rc.1 / 0.1.7-rc.2 / 0.2.0-rc.1 / 0.2.0-rc.2,逐版本证据见 [DSH-COMPATIBILITY.md](DSH-COMPATIBILITY.md))
|
|
209
|
+
- Web 端的「Gate 注入」开关需要 **dsh ≥ 0.1.7-alpha.1**(设置服务必须能投影即时字段)。更早的 dsh 上插件与 8 个 skill 照常加载、照常注入,只是开关不出现、**也不报错**:schemastery 早于 3.18.3 时该字段退化为普通布尔值;设置服务没有 `whileServed` 时客户端半侧不注册任何东西。
|
|
209
210
|
- 无外部依赖
|
|
210
211
|
|
|
211
212
|
## 配置
|
|
@@ -217,6 +218,8 @@ cp -r embedded-workbench/skills/* .zcode/skills/
|
|
|
217
218
|
| `enabled` | boolean | `true` | 设为 `false` 可完全关闭首步 Gate 注入;技能注册不受影响。 |
|
|
218
219
|
| `gateContent` | string | 内置 gate 文本 | 覆盖注入到首轮模型上下文中的文本。 |
|
|
219
220
|
|
|
221
|
+
在 dsh Web GUI 里可以直接改这个开关:侧边栏 **插件** → 本插件卡片 → 「Gate 注入」。它实时生效,不必重启 profile,而且只管注入的那段文本——关掉后 8 个技能照常注册。同一张卡片上还有一个更粗粒度的行开关:关掉它会整行卸载插件(技能和这个开关一起消失)。要持久化覆盖,仍按下面的 profile patch 写。
|
|
222
|
+
|
|
220
223
|
在 profile 的 `cordis.patch.yml` 中按 row id 覆盖(下面的例子自定义 Gate 文本):
|
|
221
224
|
|
|
222
225
|
```yaml
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* embedded-workbench — browser half: the gate-injection switch on the dsh Web
|
|
3
|
+
* client's Plugins page.
|
|
4
|
+
*
|
|
5
|
+
* The Plugins page (`@deepseek-ai/dsh-client-ui-plugin-manager`) owns the
|
|
6
|
+
* sidebar **Plugins** entry and declares the slots a bundle's own configuration
|
|
7
|
+
* registers into. This module contributes one `plugins.bundle.config` entry,
|
|
8
|
+
* keyed by this package's npm name, so the switch renders on
|
|
9
|
+
* embedded-workbench's own page between its description and its rows.
|
|
10
|
+
*
|
|
11
|
+
* Why the switch writes through `configForms` rather than reaching for the
|
|
12
|
+
* profile file: dsh's settings service exposes only the Config fields declared
|
|
13
|
+
* `.volatile()`, and it rejects a write to any other path. `enabled` is such a
|
|
14
|
+
* field (see `src/index.ts`), so flipping the switch is an ordinary
|
|
15
|
+
* revision-fenced settings write that the running Host picks up in place — the
|
|
16
|
+
* injection there re-reads the reference on every model step.
|
|
17
|
+
*
|
|
18
|
+
* Shape: this is a prebuilt module-system bundle, not a source module. It calls
|
|
19
|
+
* `window.__ModuleLoader__.load({ id, factory })` with this package's resolved
|
|
20
|
+
* npm name, and `factory` returns the cordis plugin face. Only the client
|
|
21
|
+
* baseline is requested (`react` and
|
|
22
|
+
* `@deepseek-ai/dsh-client-ui-primitives`); every other capability arrives
|
|
23
|
+
* through cordis `inject`. `scripts/build-client.mjs` publishes this file
|
|
24
|
+
* verbatim as `lib/client.js`.
|
|
25
|
+
*
|
|
26
|
+
* @module dsh-embedded-workbench/client
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
window.__ModuleLoader__.load({
|
|
30
|
+
id: 'dsh-embedded-workbench',
|
|
31
|
+
factory: (require) => {
|
|
32
|
+
const React = require('react')
|
|
33
|
+
const { Button, Switch } = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
34
|
+
|
|
35
|
+
/** Settings namespace: the Loader entry id this bundle's patch declares. */
|
|
36
|
+
const NS = 'embedded-workbench'
|
|
37
|
+
/** `plugins.bundle.config` key: the bundle's npm package name. */
|
|
38
|
+
const PACKAGE = 'dsh-embedded-workbench'
|
|
39
|
+
/** This page's dictionary namespace. */
|
|
40
|
+
const LOCALE_NS = 'embedded-workbench.plugins'
|
|
41
|
+
/** The Config field the switch writes inside the namespace's section. */
|
|
42
|
+
const FIELD = 'enabled'
|
|
43
|
+
|
|
44
|
+
/** English copy. */
|
|
45
|
+
const en = {
|
|
46
|
+
title: 'Gate injection',
|
|
47
|
+
label: 'Inject the gate text',
|
|
48
|
+
hint: 'Folds the Plan Verification Gate and the context-budget rule into the first model step of every session. Turning it off leaves all eight skills registered — only the injected text is dropped.',
|
|
49
|
+
overridden: 'Overridden',
|
|
50
|
+
reset: 'Reset to default',
|
|
51
|
+
readOnly: 'This deployment stores settings read-only.',
|
|
52
|
+
unavailable: 'This plugin is not loaded, so it cannot be configured right now.',
|
|
53
|
+
saveFailed: 'The deployment did not accept that value; the switch shows what is stored.',
|
|
54
|
+
}
|
|
55
|
+
/** Simplified Chinese copy. */
|
|
56
|
+
const zh = {
|
|
57
|
+
title: 'Gate 注入',
|
|
58
|
+
label: '注入 gate 文本',
|
|
59
|
+
hint: '把 Plan Verification Gate 与上下文预算规则折进每个会话的第一个模型步。关掉后 8 个技能仍然注册,只是不再注入那段提示文本。',
|
|
60
|
+
overridden: '已覆盖',
|
|
61
|
+
reset: '恢复默认',
|
|
62
|
+
readOnly: '本部署的设置为只读。',
|
|
63
|
+
unavailable: '该插件当前未加载,暂时无法配置。',
|
|
64
|
+
saveFailed: '本部署没有接受这个值,开关显示的是已存下的状态。',
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Required cordis services. */
|
|
68
|
+
const inject = ['slots', 'locale', 'configForms']
|
|
69
|
+
|
|
70
|
+
const GROUP = { display: 'flex', flexDirection: 'column', gap: '8px' }
|
|
71
|
+
const TITLE = { margin: 0, fontSize: '14px', fontWeight: '500', lineHeight: '22px' }
|
|
72
|
+
const ROW = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '16px' }
|
|
73
|
+
const LABEL = { fontSize: '13px', lineHeight: '20px' }
|
|
74
|
+
const NOTE = { margin: 0, fontSize: '12px', lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)' }
|
|
75
|
+
const FAILED = { margin: 0, fontSize: '12px', lineHeight: '18px', color: 'var(--dsw-alias-state-error-primary)' }
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Whether a settings-layer value carries this field, which is what marks it
|
|
79
|
+
* overridden: an override equal to the default is still an override.
|
|
80
|
+
* @param layer - the raw user layer the form snapshot carries.
|
|
81
|
+
* @returns whether the layer holds the field.
|
|
82
|
+
*/
|
|
83
|
+
function carries(layer) {
|
|
84
|
+
return layer !== null && typeof layer === 'object' && Object.prototype.hasOwnProperty.call(layer, FIELD)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Render the gate-injection switch, or the note saying why it cannot render.
|
|
89
|
+
* @param props - the page's `t` seat, the bound form snapshot hook, and the write actions.
|
|
90
|
+
* @returns the body of this bundle's configuration section.
|
|
91
|
+
*/
|
|
92
|
+
function InjectionCard(props) {
|
|
93
|
+
const t = props.t
|
|
94
|
+
const state = props.useInjectionForm((snapshot) => snapshot)
|
|
95
|
+
const [pending, setPending] = React.useState(false)
|
|
96
|
+
const [failed, setFailed] = React.useState(false)
|
|
97
|
+
|
|
98
|
+
/** Run one settings write and report a refusal or a transport failure. */
|
|
99
|
+
const write = (run) => {
|
|
100
|
+
setPending(true)
|
|
101
|
+
setFailed(false)
|
|
102
|
+
Promise.resolve(run()).then(
|
|
103
|
+
(accepted) => {
|
|
104
|
+
setPending(false)
|
|
105
|
+
setFailed(accepted === false)
|
|
106
|
+
},
|
|
107
|
+
() => {
|
|
108
|
+
setPending(false)
|
|
109
|
+
setFailed(true)
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (state.status !== 'ready') {
|
|
115
|
+
return React.createElement('p', { style: NOTE }, t('unavailable'))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const section = state.value !== null && typeof state.value === 'object' ? state.value : {}
|
|
119
|
+
// The schema default is `true`; only an explicit false means off.
|
|
120
|
+
const checked = section[FIELD] !== false
|
|
121
|
+
const overridden = carries(state.user)
|
|
122
|
+
const locked = state.writable !== true || pending
|
|
123
|
+
|
|
124
|
+
const children = [
|
|
125
|
+
React.createElement('h4', { key: 'title', style: TITLE }, t('title')),
|
|
126
|
+
React.createElement('div', { key: 'row', style: ROW }, [
|
|
127
|
+
React.createElement('span', { key: 'label', style: LABEL }, t('label')),
|
|
128
|
+
React.createElement(Switch, {
|
|
129
|
+
key: 'switch',
|
|
130
|
+
checked,
|
|
131
|
+
disabled: locked,
|
|
132
|
+
label: t('label'),
|
|
133
|
+
onChange: (next) => write(() => props.setEnabled(next)),
|
|
134
|
+
}),
|
|
135
|
+
]),
|
|
136
|
+
React.createElement('p', { key: 'hint', style: NOTE }, state.writable === true ? t('hint') : t('readOnly')),
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
if (overridden) {
|
|
140
|
+
children.push(
|
|
141
|
+
React.createElement('div', { key: 'overridden', style: ROW }, [
|
|
142
|
+
React.createElement('span', { key: 'badge', style: NOTE }, t('overridden')),
|
|
143
|
+
React.createElement(
|
|
144
|
+
Button,
|
|
145
|
+
{
|
|
146
|
+
key: 'reset',
|
|
147
|
+
variant: 'outline',
|
|
148
|
+
size: 'sm',
|
|
149
|
+
disabled: locked,
|
|
150
|
+
onClick: () => write(() => props.resetEnabled()),
|
|
151
|
+
},
|
|
152
|
+
t('reset'),
|
|
153
|
+
),
|
|
154
|
+
]),
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (failed) {
|
|
159
|
+
children.push(React.createElement('p', { key: 'failed', style: FAILED, role: 'alert' }, t('saveFailed')))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return React.createElement('div', { style: GROUP }, children)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Mount the switch while the Host serves embedded-workbench's settings
|
|
167
|
+
* namespace.
|
|
168
|
+
* @param ctx - the browser plugin context.
|
|
169
|
+
*/
|
|
170
|
+
function apply(ctx) {
|
|
171
|
+
ctx.effect(() => ctx.locale.register(LOCALE_NS, { zh, en }), 'dsh-embedded-workbench: dictionaries')
|
|
172
|
+
// `whileServed` is the registration barrier that keeps this page alive only
|
|
173
|
+
// while the Host serves the namespace. Hosts predating it (measured: dsh
|
|
174
|
+
// 0.1.5-rc.3 and 0.1.6-alpha.2) have no live settings field to offer at all,
|
|
175
|
+
// so there is nothing to register — and calling it there would throw during
|
|
176
|
+
// this plugin's own activation, which the Web boot audit then reports as a
|
|
177
|
+
// failed client entry. Degrade to no page instead.
|
|
178
|
+
if (typeof ctx.configForms.whileServed !== 'function') return
|
|
179
|
+
// The page renders the section only for a bundle whose package name is in
|
|
180
|
+
// its configuration ledger, and the ledger follows this registration. The
|
|
181
|
+
// registration in turn waits for the namespace to be served, so a profile
|
|
182
|
+
// whose embedded-workbench row is switched off shows no trace of the
|
|
183
|
+
// switch.
|
|
184
|
+
ctx.effect(
|
|
185
|
+
() =>
|
|
186
|
+
ctx.configForms.whileServed([NS], () => {
|
|
187
|
+
const form = ctx.configForms.get(NS)
|
|
188
|
+
const source = {
|
|
189
|
+
getSnapshot: () => form.getSnapshot(),
|
|
190
|
+
subscribe: (listener) => form.subscribe(listener),
|
|
191
|
+
}
|
|
192
|
+
return ctx.slots.inject('plugins.bundle.config', () =>
|
|
193
|
+
ctx.slots.register(
|
|
194
|
+
{
|
|
195
|
+
name: 'plugins.bundle.config',
|
|
196
|
+
key: PACKAGE,
|
|
197
|
+
locale: LOCALE_NS,
|
|
198
|
+
inject: () => ({
|
|
199
|
+
hooks: { injectionForm: source },
|
|
200
|
+
setEnabled: (next) => form.set(FIELD, next),
|
|
201
|
+
resetEnabled: () => form.unset(FIELD),
|
|
202
|
+
}),
|
|
203
|
+
},
|
|
204
|
+
InjectionCard,
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
}),
|
|
208
|
+
'dsh-embedded-workbench: gate-injection switch',
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { inject, apply }
|
|
213
|
+
},
|
|
214
|
+
})
|
package/lib/index.js
CHANGED
|
@@ -51,13 +51,49 @@ Plugin embedded-workbench is active: embedded C/C++ firmware development skills
|
|
|
51
51
|
|
|
52
52
|
To load the workflows and engineering policies behind these skills: load the embedded-workbench skill.
|
|
53
53
|
</EXTREMELY_IMPORTANT>`;
|
|
54
|
+
/**
|
|
55
|
+
* Declare a field as live where this host's schemastery can — `.volatile()`
|
|
56
|
+
* arrived in 3.18.3 — and leave it an ordinary field where it cannot.
|
|
57
|
+
*
|
|
58
|
+
* The fallback is load-bearing, not defensive padding. `Config` below is built
|
|
59
|
+
* while this module is still being evaluated, so an unconditional `.volatile()`
|
|
60
|
+
* on a host shipping schemastery 3.18.2 (measured: dsh 0.1.5-rc.2 and
|
|
61
|
+
* 0.1.5-rc.3) throws during import; the loader entry then fails and takes the
|
|
62
|
+
* WHOLE plugin tree — and the host's boot — down with it. Degrading costs only
|
|
63
|
+
* the Plugins-page switch, because the settings service projects nothing but
|
|
64
|
+
* fields under a `.volatile()` node; the skills and the gate injection are
|
|
65
|
+
* untouched. The returned schema keeps the plain field's static type; the
|
|
66
|
+
* `Config` interface below carries the union the host actually hands over.
|
|
67
|
+
*/
|
|
68
|
+
function live(field) {
|
|
69
|
+
const probe = field;
|
|
70
|
+
return typeof probe.volatile === 'function' ? probe.volatile() : field;
|
|
71
|
+
}
|
|
54
72
|
export const Config = z.object({
|
|
73
|
+
// Live so the Web Plugins page can flip the gate injection inside a running
|
|
74
|
+
// session: dsh's settings service projects ONLY fields under a `.volatile()`
|
|
75
|
+
// node and rejects writes to every other path. The price is that the injection
|
|
76
|
+
// reads the reference per step instead of deciding once at mount, which is also
|
|
77
|
+
// what lets a toggle take effect without remounting the row.
|
|
78
|
+
//
|
|
55
79
|
// On by default, but deliberately small: the payload carries the verification
|
|
56
80
|
// gate and the context-budget rule only, so leaving it on costs a few hundred
|
|
57
81
|
// tokens once per session rather than the ~900 of the previous payload.
|
|
58
|
-
enabled: z.boolean().default(true),
|
|
82
|
+
enabled: live(z.boolean().default(true)),
|
|
83
|
+
// Not volatile, deliberately: the settings projection feeds a GUI form, and a
|
|
84
|
+
// multi-kilobyte text field does not belong in one. Override it in the
|
|
85
|
+
// profile's `cordis.patch.yml` row instead.
|
|
59
86
|
gateContent: z.string().default(DEFAULT_GATE_CONTENT),
|
|
60
87
|
});
|
|
88
|
+
/**
|
|
89
|
+
* Read the injection switch as a boolean, whichever shape this host produced.
|
|
90
|
+
* @param config - the resolved plugin configuration.
|
|
91
|
+
* @returns whether the gate may be injected.
|
|
92
|
+
*/
|
|
93
|
+
function injectionEnabled(config) {
|
|
94
|
+
const value = config.enabled;
|
|
95
|
+
return typeof value === 'boolean' ? value : value.get();
|
|
96
|
+
}
|
|
61
97
|
function gateMessage(text) {
|
|
62
98
|
return createUserMessage({
|
|
63
99
|
content: [{ type: 'text', text }],
|
|
@@ -111,7 +147,7 @@ function inspectProvider(config) {
|
|
|
111
147
|
query: async (method) => {
|
|
112
148
|
if (method === 'status') {
|
|
113
149
|
return {
|
|
114
|
-
enabled: config
|
|
150
|
+
enabled: injectionEnabled(config),
|
|
115
151
|
gateContentLength: config.gateContent.length,
|
|
116
152
|
};
|
|
117
153
|
}
|
|
@@ -155,8 +191,12 @@ export function apply(ctx, config) {
|
|
|
155
191
|
customSkillDirs: [SKILLS_DIR],
|
|
156
192
|
});
|
|
157
193
|
});
|
|
158
|
-
|
|
159
|
-
|
|
194
|
+
// Injection listens unconditionally, including while the switch is off: the
|
|
195
|
+
// switch is volatile, so `apply` runs once and the value behind it can turn on
|
|
196
|
+
// later from the Web Plugins page. Returning early on a false value here would
|
|
197
|
+
// freeze that decision for the lifetime of the mount, and turning the switch
|
|
198
|
+
// back on could never take effect without a profile restart.
|
|
199
|
+
//
|
|
160
200
|
// Inject the gate once per session on the FIRST model step that runs,
|
|
161
201
|
// instead of at session-start: session-start injection lands in the agent's
|
|
162
202
|
// inbox, which a blank-session preset switch (agentPreset.select ->
|
|
@@ -173,6 +213,8 @@ export function apply(ctx, config) {
|
|
|
173
213
|
if (decision.kind === 'reject')
|
|
174
214
|
return decision;
|
|
175
215
|
registerProvider();
|
|
216
|
+
if (!injectionEnabled(config))
|
|
217
|
+
return decision;
|
|
176
218
|
if (gateInHistory(agent.session))
|
|
177
219
|
return decision;
|
|
178
220
|
return {
|
package/lib/types/index.d.ts
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
*
|
|
28
28
|
* @module embedded-workbench-dsh
|
|
29
29
|
*/
|
|
30
|
-
import type { Context } from '@deepseek-ai/cordis';
|
|
30
|
+
import type { Context, Volatile } from '@deepseek-ai/cordis';
|
|
31
31
|
import z from '@deepseek-ai/schemastery';
|
|
32
32
|
import type { ContextFormed } from '@deepseek-ai/dsh-llm';
|
|
33
33
|
declare module '@deepseek-ai/dsh-llm' {
|
|
@@ -40,7 +40,13 @@ declare module '@deepseek-ai/dsh-llm' {
|
|
|
40
40
|
export declare const name = "embedded-workbench";
|
|
41
41
|
export declare const inject: string[];
|
|
42
42
|
export interface Config {
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* The injection switch the Web client's Plugins page edits live: a `Volatile`
|
|
45
|
+
* reference on a host whose schemastery supports one, an ordinary boolean on a
|
|
46
|
+
* host that predates `.volatile()`. Read it through {@link injectionEnabled},
|
|
47
|
+
* which accepts both shapes.
|
|
48
|
+
*/
|
|
49
|
+
enabled: Volatile<boolean> | boolean;
|
|
44
50
|
gateContent: string;
|
|
45
51
|
}
|
|
46
52
|
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-embedded-workbench",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Embedded C/C++ firmware development toolbox — 4 agents, 8 skills covering FreeRTOS, ISR, NVM storage, Keil MDK, ARMCLANG, HardFault, state machines, architecture, LVGL patterns, and claim fact-checking. Ships a native DeepSeek Harness (dsh) bundle that folds a short gate — the Plan Verification Gate plus a context-budget rule — into the first model step.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"default": "./lib/index.js"
|
|
12
12
|
},
|
|
13
13
|
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
14
|
+
"./client": "./lib/client.js",
|
|
14
15
|
"./package.json": "./package.json"
|
|
15
16
|
},
|
|
16
17
|
"files": [
|
|
@@ -31,48 +32,59 @@
|
|
|
31
32
|
"bundle": {
|
|
32
33
|
"patch": "./cordis.patch.yml"
|
|
33
34
|
},
|
|
35
|
+
"client": {
|
|
36
|
+
"platform": "web",
|
|
37
|
+
"inject": [
|
|
38
|
+
"@deepseek-ai/dsh-client-locale",
|
|
39
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
40
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
34
43
|
"compatibility": {
|
|
35
|
-
"dsh": "^0.1.0-rc.7 || ^0.1.1-rc.1 || ^0.1.2-alpha.2 || ^0.1.2-alpha.3 || ^0.1.2-alpha.4 || ^0.1.2-alpha.5 || ^0.1.2-rc.1 || ^0.1.3-alpha.1 || ^0.1.3-alpha.2 || ^0.1.5-alpha.1 || ^0.1.5-rc.1 || ^0.1.5-alpha.2 || ^0.1.5-rc.2 || ^0.1.5-rc.3 || ^0.1.6-alpha.1 || ^0.1.6-alpha.2 || ^0.1.7-alpha.1 || ^0.1.7-alpha.2 || ^0.1.7-rc.1 || ^0.1.7-rc.2",
|
|
44
|
+
"dsh": "^0.1.0-rc.7 || ^0.1.1-rc.1 || ^0.1.2-alpha.2 || ^0.1.2-alpha.3 || ^0.1.2-alpha.4 || ^0.1.2-alpha.5 || ^0.1.2-rc.1 || ^0.1.3-alpha.1 || ^0.1.3-alpha.2 || ^0.1.5-alpha.1 || ^0.1.5-rc.1 || ^0.1.5-alpha.2 || ^0.1.5-rc.2 || ^0.1.5-rc.3 || ^0.1.6-alpha.1 || ^0.1.6-alpha.2 || ^0.1.7-alpha.1 || ^0.1.7-alpha.2 || ^0.1.7-rc.1 || ^0.1.7-rc.2 || ^0.2.0-rc.1",
|
|
36
45
|
"dshReleases": {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
46
|
+
"0.1.0-rc.7": "compatible",
|
|
47
|
+
"0.1.0-rc.8": "compatible",
|
|
48
|
+
"0.1.1-rc.1": "compatible",
|
|
49
|
+
"0.1.1-rc.2": "compatible",
|
|
50
|
+
"0.1.2-alpha.2": "compatible",
|
|
51
|
+
"0.1.2-alpha.3": "compatible",
|
|
52
|
+
"0.1.2-alpha.4": "compatible",
|
|
53
|
+
"0.1.2-alpha.5": "compatible",
|
|
54
|
+
"0.1.2-rc.1": "compatible",
|
|
55
|
+
"0.1.3-alpha.1": "compatible",
|
|
56
|
+
"0.1.3-alpha.2": "compatible",
|
|
57
|
+
"0.1.5-alpha.1": "compatible",
|
|
58
|
+
"0.1.5-alpha.2": "compatible",
|
|
59
|
+
"0.1.5-rc.1": "compatible",
|
|
60
|
+
"0.1.5-rc.2": "compatible",
|
|
61
|
+
"0.1.5-rc.3": "compatible",
|
|
62
|
+
"0.1.6-alpha.1": "compatible",
|
|
63
|
+
"0.1.6-alpha.2": "compatible",
|
|
64
|
+
"0.1.7-alpha.1": "compatible",
|
|
65
|
+
"0.1.7-alpha.2": "compatible",
|
|
66
|
+
"0.1.7-rc.1": "compatible",
|
|
67
|
+
"0.1.7-rc.2": "compatible",
|
|
68
|
+
"0.2.0-rc.1": "compatible",
|
|
69
|
+
"0.2.0-rc.2": "compatible"
|
|
59
70
|
},
|
|
60
71
|
"profiles": [
|
|
61
|
-
"headless"
|
|
72
|
+
"headless",
|
|
73
|
+
"web"
|
|
62
74
|
]
|
|
63
75
|
}
|
|
64
76
|
},
|
|
65
77
|
"scripts": {
|
|
66
|
-
"build": "tsc -p tsconfig.json",
|
|
78
|
+
"build": "tsc -p tsconfig.json && node scripts/build-client.mjs",
|
|
67
79
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
68
80
|
"bump": "node scripts/bump-version.mjs"
|
|
69
81
|
},
|
|
70
82
|
"peerDependencies": {
|
|
71
83
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
72
|
-
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
|
|
73
|
-
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
74
|
-
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
|
|
75
|
-
"@deepseek-ai/dsh-skill-filesystem": "^0.1.0-rc.8",
|
|
84
|
+
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6 || ^0.2.0-rc.1",
|
|
85
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.2.0-rc.1",
|
|
86
|
+
"@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.2.0-rc.1",
|
|
87
|
+
"@deepseek-ai/dsh-skill-filesystem": "^0.1.0-rc.8 || ^0.2.0-rc.1",
|
|
76
88
|
"@deepseek-ai/schemastery": "^3.18.4"
|
|
77
89
|
},
|
|
78
90
|
"devDependencies": {
|
|
@@ -95,7 +107,10 @@
|
|
|
95
107
|
"url": "https://github.com/AmethystLuna"
|
|
96
108
|
},
|
|
97
109
|
"license": "MIT",
|
|
98
|
-
"repository":
|
|
110
|
+
"repository": {
|
|
111
|
+
"type": "git",
|
|
112
|
+
"url": "git+https://github.com/AmethystLuna/embedded-workbench.git"
|
|
113
|
+
},
|
|
99
114
|
"keywords": [
|
|
100
115
|
"embedded",
|
|
101
116
|
"firmware",
|
package/src/client.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* embedded-workbench — browser half: the gate-injection switch on the dsh Web
|
|
3
|
+
* client's Plugins page.
|
|
4
|
+
*
|
|
5
|
+
* The Plugins page (`@deepseek-ai/dsh-client-ui-plugin-manager`) owns the
|
|
6
|
+
* sidebar **Plugins** entry and declares the slots a bundle's own configuration
|
|
7
|
+
* registers into. This module contributes one `plugins.bundle.config` entry,
|
|
8
|
+
* keyed by this package's npm name, so the switch renders on
|
|
9
|
+
* embedded-workbench's own page between its description and its rows.
|
|
10
|
+
*
|
|
11
|
+
* Why the switch writes through `configForms` rather than reaching for the
|
|
12
|
+
* profile file: dsh's settings service exposes only the Config fields declared
|
|
13
|
+
* `.volatile()`, and it rejects a write to any other path. `enabled` is such a
|
|
14
|
+
* field (see `src/index.ts`), so flipping the switch is an ordinary
|
|
15
|
+
* revision-fenced settings write that the running Host picks up in place — the
|
|
16
|
+
* injection there re-reads the reference on every model step.
|
|
17
|
+
*
|
|
18
|
+
* Shape: this is a prebuilt module-system bundle, not a source module. It calls
|
|
19
|
+
* `window.__ModuleLoader__.load({ id, factory })` with this package's resolved
|
|
20
|
+
* npm name, and `factory` returns the cordis plugin face. Only the client
|
|
21
|
+
* baseline is requested (`react` and
|
|
22
|
+
* `@deepseek-ai/dsh-client-ui-primitives`); every other capability arrives
|
|
23
|
+
* through cordis `inject`. `scripts/build-client.mjs` publishes this file
|
|
24
|
+
* verbatim as `lib/client.js`.
|
|
25
|
+
*
|
|
26
|
+
* @module dsh-embedded-workbench/client
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
window.__ModuleLoader__.load({
|
|
30
|
+
id: 'dsh-embedded-workbench',
|
|
31
|
+
factory: (require) => {
|
|
32
|
+
const React = require('react')
|
|
33
|
+
const { Button, Switch } = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
34
|
+
|
|
35
|
+
/** Settings namespace: the Loader entry id this bundle's patch declares. */
|
|
36
|
+
const NS = 'embedded-workbench'
|
|
37
|
+
/** `plugins.bundle.config` key: the bundle's npm package name. */
|
|
38
|
+
const PACKAGE = 'dsh-embedded-workbench'
|
|
39
|
+
/** This page's dictionary namespace. */
|
|
40
|
+
const LOCALE_NS = 'embedded-workbench.plugins'
|
|
41
|
+
/** The Config field the switch writes inside the namespace's section. */
|
|
42
|
+
const FIELD = 'enabled'
|
|
43
|
+
|
|
44
|
+
/** English copy. */
|
|
45
|
+
const en = {
|
|
46
|
+
title: 'Gate injection',
|
|
47
|
+
label: 'Inject the gate text',
|
|
48
|
+
hint: 'Folds the Plan Verification Gate and the context-budget rule into the first model step of every session. Turning it off leaves all eight skills registered — only the injected text is dropped.',
|
|
49
|
+
overridden: 'Overridden',
|
|
50
|
+
reset: 'Reset to default',
|
|
51
|
+
readOnly: 'This deployment stores settings read-only.',
|
|
52
|
+
unavailable: 'This plugin is not loaded, so it cannot be configured right now.',
|
|
53
|
+
saveFailed: 'The deployment did not accept that value; the switch shows what is stored.',
|
|
54
|
+
}
|
|
55
|
+
/** Simplified Chinese copy. */
|
|
56
|
+
const zh = {
|
|
57
|
+
title: 'Gate 注入',
|
|
58
|
+
label: '注入 gate 文本',
|
|
59
|
+
hint: '把 Plan Verification Gate 与上下文预算规则折进每个会话的第一个模型步。关掉后 8 个技能仍然注册,只是不再注入那段提示文本。',
|
|
60
|
+
overridden: '已覆盖',
|
|
61
|
+
reset: '恢复默认',
|
|
62
|
+
readOnly: '本部署的设置为只读。',
|
|
63
|
+
unavailable: '该插件当前未加载,暂时无法配置。',
|
|
64
|
+
saveFailed: '本部署没有接受这个值,开关显示的是已存下的状态。',
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Required cordis services. */
|
|
68
|
+
const inject = ['slots', 'locale', 'configForms']
|
|
69
|
+
|
|
70
|
+
const GROUP = { display: 'flex', flexDirection: 'column', gap: '8px' }
|
|
71
|
+
const TITLE = { margin: 0, fontSize: '14px', fontWeight: '500', lineHeight: '22px' }
|
|
72
|
+
const ROW = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '16px' }
|
|
73
|
+
const LABEL = { fontSize: '13px', lineHeight: '20px' }
|
|
74
|
+
const NOTE = { margin: 0, fontSize: '12px', lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)' }
|
|
75
|
+
const FAILED = { margin: 0, fontSize: '12px', lineHeight: '18px', color: 'var(--dsw-alias-state-error-primary)' }
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Whether a settings-layer value carries this field, which is what marks it
|
|
79
|
+
* overridden: an override equal to the default is still an override.
|
|
80
|
+
* @param layer - the raw user layer the form snapshot carries.
|
|
81
|
+
* @returns whether the layer holds the field.
|
|
82
|
+
*/
|
|
83
|
+
function carries(layer) {
|
|
84
|
+
return layer !== null && typeof layer === 'object' && Object.prototype.hasOwnProperty.call(layer, FIELD)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Render the gate-injection switch, or the note saying why it cannot render.
|
|
89
|
+
* @param props - the page's `t` seat, the bound form snapshot hook, and the write actions.
|
|
90
|
+
* @returns the body of this bundle's configuration section.
|
|
91
|
+
*/
|
|
92
|
+
function InjectionCard(props) {
|
|
93
|
+
const t = props.t
|
|
94
|
+
const state = props.useInjectionForm((snapshot) => snapshot)
|
|
95
|
+
const [pending, setPending] = React.useState(false)
|
|
96
|
+
const [failed, setFailed] = React.useState(false)
|
|
97
|
+
|
|
98
|
+
/** Run one settings write and report a refusal or a transport failure. */
|
|
99
|
+
const write = (run) => {
|
|
100
|
+
setPending(true)
|
|
101
|
+
setFailed(false)
|
|
102
|
+
Promise.resolve(run()).then(
|
|
103
|
+
(accepted) => {
|
|
104
|
+
setPending(false)
|
|
105
|
+
setFailed(accepted === false)
|
|
106
|
+
},
|
|
107
|
+
() => {
|
|
108
|
+
setPending(false)
|
|
109
|
+
setFailed(true)
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (state.status !== 'ready') {
|
|
115
|
+
return React.createElement('p', { style: NOTE }, t('unavailable'))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const section = state.value !== null && typeof state.value === 'object' ? state.value : {}
|
|
119
|
+
// The schema default is `true`; only an explicit false means off.
|
|
120
|
+
const checked = section[FIELD] !== false
|
|
121
|
+
const overridden = carries(state.user)
|
|
122
|
+
const locked = state.writable !== true || pending
|
|
123
|
+
|
|
124
|
+
const children = [
|
|
125
|
+
React.createElement('h4', { key: 'title', style: TITLE }, t('title')),
|
|
126
|
+
React.createElement('div', { key: 'row', style: ROW }, [
|
|
127
|
+
React.createElement('span', { key: 'label', style: LABEL }, t('label')),
|
|
128
|
+
React.createElement(Switch, {
|
|
129
|
+
key: 'switch',
|
|
130
|
+
checked,
|
|
131
|
+
disabled: locked,
|
|
132
|
+
label: t('label'),
|
|
133
|
+
onChange: (next) => write(() => props.setEnabled(next)),
|
|
134
|
+
}),
|
|
135
|
+
]),
|
|
136
|
+
React.createElement('p', { key: 'hint', style: NOTE }, state.writable === true ? t('hint') : t('readOnly')),
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
if (overridden) {
|
|
140
|
+
children.push(
|
|
141
|
+
React.createElement('div', { key: 'overridden', style: ROW }, [
|
|
142
|
+
React.createElement('span', { key: 'badge', style: NOTE }, t('overridden')),
|
|
143
|
+
React.createElement(
|
|
144
|
+
Button,
|
|
145
|
+
{
|
|
146
|
+
key: 'reset',
|
|
147
|
+
variant: 'outline',
|
|
148
|
+
size: 'sm',
|
|
149
|
+
disabled: locked,
|
|
150
|
+
onClick: () => write(() => props.resetEnabled()),
|
|
151
|
+
},
|
|
152
|
+
t('reset'),
|
|
153
|
+
),
|
|
154
|
+
]),
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (failed) {
|
|
159
|
+
children.push(React.createElement('p', { key: 'failed', style: FAILED, role: 'alert' }, t('saveFailed')))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return React.createElement('div', { style: GROUP }, children)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Mount the switch while the Host serves embedded-workbench's settings
|
|
167
|
+
* namespace.
|
|
168
|
+
* @param ctx - the browser plugin context.
|
|
169
|
+
*/
|
|
170
|
+
function apply(ctx) {
|
|
171
|
+
ctx.effect(() => ctx.locale.register(LOCALE_NS, { zh, en }), 'dsh-embedded-workbench: dictionaries')
|
|
172
|
+
// `whileServed` is the registration barrier that keeps this page alive only
|
|
173
|
+
// while the Host serves the namespace. Hosts predating it (measured: dsh
|
|
174
|
+
// 0.1.5-rc.3 and 0.1.6-alpha.2) have no live settings field to offer at all,
|
|
175
|
+
// so there is nothing to register — and calling it there would throw during
|
|
176
|
+
// this plugin's own activation, which the Web boot audit then reports as a
|
|
177
|
+
// failed client entry. Degrade to no page instead.
|
|
178
|
+
if (typeof ctx.configForms.whileServed !== 'function') return
|
|
179
|
+
// The page renders the section only for a bundle whose package name is in
|
|
180
|
+
// its configuration ledger, and the ledger follows this registration. The
|
|
181
|
+
// registration in turn waits for the namespace to be served, so a profile
|
|
182
|
+
// whose embedded-workbench row is switched off shows no trace of the
|
|
183
|
+
// switch.
|
|
184
|
+
ctx.effect(
|
|
185
|
+
() =>
|
|
186
|
+
ctx.configForms.whileServed([NS], () => {
|
|
187
|
+
const form = ctx.configForms.get(NS)
|
|
188
|
+
const source = {
|
|
189
|
+
getSnapshot: () => form.getSnapshot(),
|
|
190
|
+
subscribe: (listener) => form.subscribe(listener),
|
|
191
|
+
}
|
|
192
|
+
return ctx.slots.inject('plugins.bundle.config', () =>
|
|
193
|
+
ctx.slots.register(
|
|
194
|
+
{
|
|
195
|
+
name: 'plugins.bundle.config',
|
|
196
|
+
key: PACKAGE,
|
|
197
|
+
locale: LOCALE_NS,
|
|
198
|
+
inject: () => ({
|
|
199
|
+
hooks: { injectionForm: source },
|
|
200
|
+
setEnabled: (next) => form.set(FIELD, next),
|
|
201
|
+
resetEnabled: () => form.unset(FIELD),
|
|
202
|
+
}),
|
|
203
|
+
},
|
|
204
|
+
InjectionCard,
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
}),
|
|
208
|
+
'dsh-embedded-workbench: gate-injection switch',
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { inject, apply }
|
|
213
|
+
},
|
|
214
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
import { fileURLToPath } from 'node:url'
|
|
32
|
-
import type { Context } from '@deepseek-ai/cordis'
|
|
32
|
+
import type { Context, Volatile } from '@deepseek-ai/cordis'
|
|
33
33
|
import z from '@deepseek-ai/schemastery'
|
|
34
34
|
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
35
35
|
import type { ContextFormed } from '@deepseek-ai/dsh-llm'
|
|
@@ -75,19 +75,68 @@ Plugin embedded-workbench is active: embedded C/C++ firmware development skills
|
|
|
75
75
|
To load the workflows and engineering policies behind these skills: load the embedded-workbench skill.
|
|
76
76
|
</EXTREMELY_IMPORTANT>`
|
|
77
77
|
|
|
78
|
+
/** A schemastery field that may or may not carry `.volatile()`. */
|
|
79
|
+
interface LiveField {
|
|
80
|
+
volatile?: () => unknown
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Declare a field as live where this host's schemastery can — `.volatile()`
|
|
85
|
+
* arrived in 3.18.3 — and leave it an ordinary field where it cannot.
|
|
86
|
+
*
|
|
87
|
+
* The fallback is load-bearing, not defensive padding. `Config` below is built
|
|
88
|
+
* while this module is still being evaluated, so an unconditional `.volatile()`
|
|
89
|
+
* on a host shipping schemastery 3.18.2 (measured: dsh 0.1.5-rc.2 and
|
|
90
|
+
* 0.1.5-rc.3) throws during import; the loader entry then fails and takes the
|
|
91
|
+
* WHOLE plugin tree — and the host's boot — down with it. Degrading costs only
|
|
92
|
+
* the Plugins-page switch, because the settings service projects nothing but
|
|
93
|
+
* fields under a `.volatile()` node; the skills and the gate injection are
|
|
94
|
+
* untouched. The returned schema keeps the plain field's static type; the
|
|
95
|
+
* `Config` interface below carries the union the host actually hands over.
|
|
96
|
+
*/
|
|
97
|
+
function live<T>(field: T): T {
|
|
98
|
+
const probe = field as T & LiveField
|
|
99
|
+
return typeof probe.volatile === 'function' ? (probe.volatile() as T) : field
|
|
100
|
+
}
|
|
101
|
+
|
|
78
102
|
export interface Config {
|
|
79
|
-
|
|
103
|
+
/**
|
|
104
|
+
* The injection switch the Web client's Plugins page edits live: a `Volatile`
|
|
105
|
+
* reference on a host whose schemastery supports one, an ordinary boolean on a
|
|
106
|
+
* host that predates `.volatile()`. Read it through {@link injectionEnabled},
|
|
107
|
+
* which accepts both shapes.
|
|
108
|
+
*/
|
|
109
|
+
enabled: Volatile<boolean> | boolean
|
|
80
110
|
gateContent: string
|
|
81
111
|
}
|
|
82
112
|
|
|
83
113
|
export const Config = z.object({
|
|
114
|
+
// Live so the Web Plugins page can flip the gate injection inside a running
|
|
115
|
+
// session: dsh's settings service projects ONLY fields under a `.volatile()`
|
|
116
|
+
// node and rejects writes to every other path. The price is that the injection
|
|
117
|
+
// reads the reference per step instead of deciding once at mount, which is also
|
|
118
|
+
// what lets a toggle take effect without remounting the row.
|
|
119
|
+
//
|
|
84
120
|
// On by default, but deliberately small: the payload carries the verification
|
|
85
121
|
// gate and the context-budget rule only, so leaving it on costs a few hundred
|
|
86
122
|
// tokens once per session rather than the ~900 of the previous payload.
|
|
87
|
-
enabled: z.boolean().default(true),
|
|
123
|
+
enabled: live(z.boolean().default(true)),
|
|
124
|
+
// Not volatile, deliberately: the settings projection feeds a GUI form, and a
|
|
125
|
+
// multi-kilobyte text field does not belong in one. Override it in the
|
|
126
|
+
// profile's `cordis.patch.yml` row instead.
|
|
88
127
|
gateContent: z.string().default(DEFAULT_GATE_CONTENT),
|
|
89
128
|
})
|
|
90
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Read the injection switch as a boolean, whichever shape this host produced.
|
|
132
|
+
* @param config - the resolved plugin configuration.
|
|
133
|
+
* @returns whether the gate may be injected.
|
|
134
|
+
*/
|
|
135
|
+
function injectionEnabled(config: Config): boolean {
|
|
136
|
+
const value = config.enabled
|
|
137
|
+
return typeof value === 'boolean' ? value : value.get()
|
|
138
|
+
}
|
|
139
|
+
|
|
91
140
|
function gateMessage(text: string): UserMessage {
|
|
92
141
|
return createUserMessage({
|
|
93
142
|
content: [{ type: 'text', text }],
|
|
@@ -158,7 +207,7 @@ function inspectProvider(config: Config): HostCordisInspectProviderRegistration
|
|
|
158
207
|
query: async (method) => {
|
|
159
208
|
if (method === 'status') {
|
|
160
209
|
return {
|
|
161
|
-
enabled: config
|
|
210
|
+
enabled: injectionEnabled(config),
|
|
162
211
|
gateContentLength: config.gateContent.length,
|
|
163
212
|
}
|
|
164
213
|
}
|
|
@@ -200,7 +249,12 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
200
249
|
customSkillDirs: [SKILLS_DIR],
|
|
201
250
|
})
|
|
202
251
|
})
|
|
203
|
-
|
|
252
|
+
// Injection listens unconditionally, including while the switch is off: the
|
|
253
|
+
// switch is volatile, so `apply` runs once and the value behind it can turn on
|
|
254
|
+
// later from the Web Plugins page. Returning early on a false value here would
|
|
255
|
+
// freeze that decision for the lifetime of the mount, and turning the switch
|
|
256
|
+
// back on could never take effect without a profile restart.
|
|
257
|
+
//
|
|
204
258
|
// Inject the gate once per session on the FIRST model step that runs,
|
|
205
259
|
// instead of at session-start: session-start injection lands in the agent's
|
|
206
260
|
// inbox, which a blank-session preset switch (agentPreset.select ->
|
|
@@ -216,6 +270,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
216
270
|
const decision = await next()
|
|
217
271
|
if (decision.kind === 'reject') return decision
|
|
218
272
|
registerProvider()
|
|
273
|
+
if (!injectionEnabled(config)) return decision
|
|
219
274
|
if (gateInHistory(agent.session)) return decision
|
|
220
275
|
return {
|
|
221
276
|
kind: 'enter',
|