dsh-embedded-workbench 0.8.12 → 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 +32 -9
- package/README.md +32 -9
- package/cordis.patch.yml +4 -0
- package/lib/client.js +214 -0
- package/lib/index.js +242 -211
- package/lib/types/index.d.ts +59 -51
- package/package.json +52 -33
- package/skills/embedded-workbench/SKILL.md +63 -78
- package/skills/embedded-workbench/references/platform-tool-mapping.md +59 -22
- package/skills/embedded-workbench/references/proactive-suggestions.md +27 -0
- package/src/client.js +214 -0
- package/src/index.ts +79 -35
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
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* embedded-workbench — DeepSeek Harness native plugin for the Embedded
|
|
3
|
-
* Workbench toolbox.
|
|
4
|
-
* Flags, Plan Verification Gate, skills roster) into the first model step
|
|
5
|
-
* of every agent session, mirroring the SessionStart hook the Claude Code
|
|
6
|
-
* plugin installs. The 8 skills ship in this package's `skills/` directory
|
|
3
|
+
* Workbench toolbox. The 8 skills ship in this package's `skills/` directory
|
|
7
4
|
* and are registered at apply time into dsh's `ctx.skills` registry through
|
|
8
5
|
* the standard filesystem provider, so they appear in every session catalog
|
|
9
|
-
* without a manual copy step.
|
|
6
|
+
* without a manual copy step. The plugin also folds a short gate text into the
|
|
7
|
+
* first model step of every agent session, mirroring the SessionStart hook the
|
|
8
|
+
* Claude Code plugin installs.
|
|
10
9
|
*
|
|
11
10
|
* Injection listens on agent/pre-step and appends the gate to the FIRST
|
|
12
11
|
* model step that runs, once per session (guarded by the session's durable
|
|
@@ -17,17 +16,20 @@
|
|
|
17
16
|
* reminders (skill catalog, AGENTS.md, gate plugins) simply defer this message
|
|
18
17
|
* to the first step after their promotion, and the history guard re-injects it
|
|
19
18
|
* there. The default gate text is the dsh-native adaptation of
|
|
20
|
-
* `hooks/session-start-content.md`: behavior rules
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
19
|
+
* `hooks/session-start-content.md`: the behavior rules stay in sync (the Plan
|
|
20
|
+
* Verification Gate and the context-budget rule), while presentation is adapted
|
|
21
|
+
* to dsh's native skill catalog — no roster table (the model sees skills in its
|
|
22
|
+
* catalog) and no install instructions (those live in `.dsh/INSTALL.md`). The
|
|
23
|
+
* payload is deliberately small: it carries the verification gate and the
|
|
24
|
+
* budget rule, not the 1% Rule / Red Flags enforcement scaffolding, which
|
|
25
|
+
* measurably pushes capable models into rigid phases and unnecessary fan-out.
|
|
26
|
+
* Deployments override via Config.
|
|
25
27
|
*
|
|
26
28
|
* @module embedded-workbench-dsh
|
|
27
29
|
*/
|
|
28
30
|
|
|
29
31
|
import { fileURLToPath } from 'node:url'
|
|
30
|
-
import type { Context } from '@deepseek-ai/cordis'
|
|
32
|
+
import type { Context, Volatile } from '@deepseek-ai/cordis'
|
|
31
33
|
import z from '@deepseek-ai/schemastery'
|
|
32
34
|
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
33
35
|
import type { ContextFormed } from '@deepseek-ai/dsh-llm'
|
|
@@ -64,41 +66,77 @@ const GATE_PLUGIN_ID = 'embedded-workbench'
|
|
|
64
66
|
const GATE_SOURCE_KIND: 'plugin:embedded-workbench' = 'plugin:embedded-workbench'
|
|
65
67
|
|
|
66
68
|
const DEFAULT_GATE_CONTENT = `<EXTREMELY_IMPORTANT>
|
|
67
|
-
Plugin embedded-workbench is active
|
|
69
|
+
Plugin embedded-workbench is active: embedded C/C++ firmware development skills are in your skill catalog. Load the one whose "Use when" matches before substantial work, with the skill tool.
|
|
68
70
|
|
|
69
|
-
**
|
|
71
|
+
**Plan Verification Gate**: before calling exit_plan_mode (or presenting a plan for approval), load the logicprobe skill — or the built-in fact-check skill when logicprobe is not installed — and append a "## Plan Verification" block to the plan. If you verify with neither, tell the user the plan is unverified before asking for approval; a silent skip is not an option. "This change is too small to check" and "I already read the code, the paths are right" are the two rationalizations this gate exists to catch.
|
|
70
72
|
|
|
71
|
-
**
|
|
73
|
+
**Context budget**: no token meter is visible to you, so never guess one. Act on what you can see — a pruned or spilled tool result means stop pulling it in whole, and a compaction checkpoint means move durable state into files. When a large step (many sources, a long sweep, several independent areas) shows no such signal, ask the user what to spend context on rather than deciding silently. If nobody can answer, take the reversible option and say so. When you do delegate, prefer \`subagent_fork\` over \`subagent\` if the sub-agent needs context you already built — its summary still lands here.
|
|
72
74
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
| "This is just a quick fix" | Quick fixes break things. A 3-line design check costs 30 seconds. |
|
|
76
|
-
| "I already understand this code" | You are looking at one file. The blast radius may span 5 modules. |
|
|
77
|
-
| "The skill is overkill for this" | Simple things become complex. Check for skills. |
|
|
78
|
-
| "Let me explore the codebase first" | Skills tell you HOW to explore. Check first. |
|
|
79
|
-
| "I can just read the file directly" | Skills have patterns and pitfalls you will not discover by reading. |
|
|
80
|
-
| "I remember this skill content" | Skills evolve. Always load the current version. |
|
|
81
|
-
| "I've explored enough, time to exit plan mode" | The exit_plan_mode tool is the verification gate. Have you loaded the logicprobe skill — or, if it is not installed, the built-in fallback fact-check skill? Every plan must pass this gate before exit. |
|
|
82
|
-
| "This plan is too simple for logicprobe" | logicprobe auto-classifies depth; the fallback fact-check verifies every claim regardless. You don't decide. |
|
|
83
|
-
| "I already read the code, I know the file paths are correct" | Load the logicprobe skill or the fallback fact-check skill, verify each claim, append the "## Plan Verification" block. |
|
|
84
|
-
|
|
85
|
-
**Plan Verification Gate**: Before calling exit_plan_mode (or presenting a plan for approval), load the logicprobe skill (a separate plugin) — or, if it is missing from your skill catalog, load the built-in fallback fact-check skill for claim-by-claim verification, tell the user that behavioral/model claims degrade to manual confirmation, and recommend installing logicprobe. If neither is loaded, inform the user "此计划未经核查,是否需要我先做事实核查?" Silent skip is not an option.
|
|
75
|
+
To load the workflows and engineering policies behind these skills: load the embedded-workbench skill.
|
|
76
|
+
</EXTREMELY_IMPORTANT>`
|
|
86
77
|
|
|
87
|
-
|
|
78
|
+
/** A schemastery field that may or may not carry `.volatile()`. */
|
|
79
|
+
interface LiveField {
|
|
80
|
+
volatile?: () => unknown
|
|
81
|
+
}
|
|
88
82
|
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
}
|
|
91
101
|
|
|
92
102
|
export interface Config {
|
|
93
|
-
|
|
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
|
|
94
110
|
gateContent: string
|
|
95
111
|
}
|
|
96
112
|
|
|
97
113
|
export const Config = z.object({
|
|
98
|
-
|
|
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
|
+
//
|
|
120
|
+
// On by default, but deliberately small: the payload carries the verification
|
|
121
|
+
// gate and the context-budget rule only, so leaving it on costs a few hundred
|
|
122
|
+
// tokens once per session rather than the ~900 of the previous payload.
|
|
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.
|
|
99
127
|
gateContent: z.string().default(DEFAULT_GATE_CONTENT),
|
|
100
128
|
})
|
|
101
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
|
+
|
|
102
140
|
function gateMessage(text: string): UserMessage {
|
|
103
141
|
return createUserMessage({
|
|
104
142
|
content: [{ type: 'text', text }],
|
|
@@ -143,7 +181,7 @@ function inspectProvider(config: Config): HostCordisInspectProviderRegistration
|
|
|
143
181
|
return {
|
|
144
182
|
manifest: {
|
|
145
183
|
id: 'embedded-workbench',
|
|
146
|
-
description: 'Session-start gate injection for the Embedded Workbench toolbox — folds the
|
|
184
|
+
description: 'Session-start gate injection for the Embedded Workbench toolbox — folds the Plan Verification Gate and the context-budget rule into the first model step of every agent session.',
|
|
147
185
|
methods: [
|
|
148
186
|
{
|
|
149
187
|
name: 'status',
|
|
@@ -169,7 +207,7 @@ function inspectProvider(config: Config): HostCordisInspectProviderRegistration
|
|
|
169
207
|
query: async (method) => {
|
|
170
208
|
if (method === 'status') {
|
|
171
209
|
return {
|
|
172
|
-
enabled: config
|
|
210
|
+
enabled: injectionEnabled(config),
|
|
173
211
|
gateContentLength: config.gateContent.length,
|
|
174
212
|
}
|
|
175
213
|
}
|
|
@@ -211,7 +249,12 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
211
249
|
customSkillDirs: [SKILLS_DIR],
|
|
212
250
|
})
|
|
213
251
|
})
|
|
214
|
-
|
|
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
|
+
//
|
|
215
258
|
// Inject the gate once per session on the FIRST model step that runs,
|
|
216
259
|
// instead of at session-start: session-start injection lands in the agent's
|
|
217
260
|
// inbox, which a blank-session preset switch (agentPreset.select ->
|
|
@@ -227,6 +270,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
227
270
|
const decision = await next()
|
|
228
271
|
if (decision.kind === 'reject') return decision
|
|
229
272
|
registerProvider()
|
|
273
|
+
if (!injectionEnabled(config)) return decision
|
|
230
274
|
if (gateInHistory(agent.session)) return decision
|
|
231
275
|
return {
|
|
232
276
|
kind: 'enter',
|