fix: show the settings card, and make an edit actually take effect
`mode` and `extraRoots` carried no `volatile()` marker, so the framework's
form projection dropped the whole entry from `settings.describe` and the plugin
had no card under Plugins at all. The marker is what creates the card.
It is also what turns each field into a reference rather than a value: the
resolved config hands `apply` a `{get(), [write]}` per volatile field, which is
how a settings write lands in a running plugin without re-applying it. So the
policy and the guard now read both fields at the point of use instead of
snapshotting them at mount, and `sectionOrder`/`contextOrder` stay unmarked
because a prompt section cannot be re-ordered without a remount.
A section is also registered once as a plain string, so the rule could not
follow a live `mode` without freezing at load. The mode-specific sentence moved
into the binding, which is a callback.
This commit is contained in:
1 parent
c41c555400
commit
28f20a8aac
8 files changed
+283
-59
No files matched your search
+25
-10
@@ -64,10 +64,19 @@ export const Config = Schema.object({
|
||||
*/
|
||||
export function apply(ctx, config) {
|
||||
const policy = ctx.facetPolicy
|
||||
/** @type {'ask'|'deny'|'off'} */
|
||||
const mode = policy.mode
|
||||
|
||||
if (mode === 'off') return
|
||||
// Read at the point of use, never captured. `mode` is a volatile field, so a
|
||||
// settings edit updates the policy's reference in place and deliberately does
|
||||
// not re-apply this plugin; a `const mode` here would answer with the value
|
||||
// the session was loaded with, for the life of the session.
|
||||
const modeNow = () => policy.mode
|
||||
|
||||
// `off` is the one part that stays a mount-time decision. Deciding it here
|
||||
// means an `off` deployment contributes no rule, no binding and no tool —
|
||||
// which is the point of the mode. The cost is that turning it *on* later needs
|
||||
// a reload, because a volatile-only edit never re-runs this function. `ask`
|
||||
// and `deny` swap freely in both directions.
|
||||
if (modeNow() === 'off') return
|
||||
|
||||
const sectionOrder = policy.sectionOrder ?? 100
|
||||
const contextOrder = policy.contextOrder ?? 105
|
||||
@@ -129,6 +138,9 @@ export function apply(ctx, config) {
|
||||
const classify = async (exec) => {
|
||||
if (exec.signal?.aborted) return undefined
|
||||
|
||||
const mode = modeNow()
|
||||
if (mode === 'off') return undefined
|
||||
|
||||
// Widening is the one act that always needs the user, whatever the mode:
|
||||
// it is explicit, deliberate, and the only thing standing between a scoped
|
||||
// agent and an unscoped one.
|
||||
@@ -155,11 +167,13 @@ export function apply(ctx, config) {
|
||||
}
|
||||
|
||||
// The rule. `interpolate: false` because every value in it is already
|
||||
// literal; there is nothing here for the harness to substitute.
|
||||
// literal; there is nothing here for the harness to substitute. It carries no
|
||||
// `mode`, because a section is registered once and cannot follow a live edit —
|
||||
// see `facetRuleText`.
|
||||
ctx.systemPrompt.section({
|
||||
name: 'facet-scope',
|
||||
order: Number(sectionOrder),
|
||||
text: facetRuleText({ mode }),
|
||||
text: facetRuleText(),
|
||||
interpolate: false,
|
||||
})
|
||||
|
||||
@@ -173,11 +187,11 @@ export function apply(ctx, config) {
|
||||
text: (context) => {
|
||||
const scope = policy.scopeFor(context?.agent)
|
||||
if (scope === undefined) return ''
|
||||
return facetContextText({ cwd: scope.cwd, root: scope.root, extras: scope.extras, mode })
|
||||
return facetContextText({ cwd: scope.cwd, root: scope.root, extras: scope.extras, mode: modeNow() })
|
||||
},
|
||||
})
|
||||
|
||||
if (config.tool) ctx.tools.register(facetTool(ctx, policy, mode))
|
||||
if (config.tool) ctx.tools.register(facetTool(ctx, policy))
|
||||
|
||||
// `next()` first, so an earlier policy plugin keeps its say: this listener
|
||||
// only ever adds a question or a refusal to a call that was otherwise allowed.
|
||||
@@ -206,9 +220,8 @@ function requestedPaths(args) {
|
||||
*
|
||||
* @param {import('@deepseek-ai/cordis').Context} ctx - the guard's context.
|
||||
* @param {object} policy - the `facetPolicy` service.
|
||||
* @param {'ask'|'deny'|'off'} mode - the deployment's enforcement mode.
|
||||
*/
|
||||
function facetTool(ctx, policy, mode) {
|
||||
function facetTool(ctx, policy) {
|
||||
return defineTool({
|
||||
name: FACET_TOOL,
|
||||
description:
|
||||
@@ -276,7 +289,9 @@ function facetTool(ctx, policy, mode) {
|
||||
ok: true,
|
||||
root: scope.root,
|
||||
facet: scope.root,
|
||||
mode,
|
||||
// Read here rather than captured at mount, so `status` reports the mode
|
||||
// in force now instead of the one this session started under.
|
||||
mode: String(policy.mode),
|
||||
standing: policy.standing,
|
||||
granted,
|
||||
scope: scope.extras,
|
||||
|
||||
Reference in new issue
Block a user