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
+64
-6
@@ -18,6 +18,38 @@
|
||||
|
||||
import { isAbsolute, resolve } from 'node:path'
|
||||
|
||||
/**
|
||||
* The symbol cosmokit uses to mark a volatile config reference.
|
||||
*
|
||||
* Looked up by symbol rather than by importing cosmokit, because the ESM and CJS
|
||||
* copies of that library are distinct module instances and `Symbol.for` is the
|
||||
* only identity they share.
|
||||
*/
|
||||
const VOLATILE_WRITE = Symbol.for('cosmokit.volatile.write')
|
||||
|
||||
/**
|
||||
* Read one configuration field.
|
||||
*
|
||||
* A `volatile()` field is a *reference* in the running config, not a plain
|
||||
* value: the framework resolves the schema through `Schema.resolve`, which wraps
|
||||
* each volatile field in `createVolatile(...)` — `{get(), [write]}`. A
|
||||
* non-volatile field, or a config object a test handed straight to
|
||||
* {@link createFacetPolicy}, is the value itself. Both are accepted, so one code
|
||||
* path serves a real profile and a direct call.
|
||||
*
|
||||
* Every read has to happen at the point of use rather than once at construction.
|
||||
* A settings write commits into these references in place and deliberately does
|
||||
* *not* re-apply the plugin, so anything captured at construction would never
|
||||
* see the change — which is how a plugin ends up with a settings card that does
|
||||
* nothing.
|
||||
*
|
||||
* @param {unknown} value - a field of the resolved config.
|
||||
* @returns {unknown} the current value.
|
||||
*/
|
||||
export function configValue(value) {
|
||||
return typeof value === 'object' && value !== null && VOLATILE_WRITE in value ? value.get() : value
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} FacetScope
|
||||
* @property {string} cwd - the session working directory, absolute.
|
||||
@@ -28,6 +60,13 @@ import { isAbsolute, resolve } from 'node:path'
|
||||
/**
|
||||
* Build the policy for one deployment.
|
||||
*
|
||||
* `config` is the *resolved* composition config, so its volatile fields are
|
||||
* references rather than values; every read below goes through
|
||||
* {@link configValue} and happens per use. Holding the config and reading it lazily
|
||||
* is what makes the settings card honest: `mode` and `extraRoots` are edited live
|
||||
* in the Web UI, and a policy that snapshotted them here would keep answering
|
||||
* with the values it was loaded with.
|
||||
*
|
||||
* @param {object} config
|
||||
* @param {'ask'|'deny'|'off'} config.mode - how an out-of-scope call is handled.
|
||||
* @param {readonly string[]} [config.extraRoots] - standing grants, resolved against each session's cwd.
|
||||
@@ -36,7 +75,20 @@ import { isAbsolute, resolve } from 'node:path'
|
||||
export function createFacetPolicy(config) {
|
||||
/** @type {Map<string, string[]>} session id → granted paths, in the order granted. */
|
||||
const grants = new Map()
|
||||
const standing = Array.isArray(config.extraRoots) ? config.extraRoots.filter(isPath) : []
|
||||
|
||||
/**
|
||||
* The deployment's standing grants, read fresh on every call.
|
||||
*
|
||||
* Not filtered once at construction: `extraRoots` is volatile, so an edit in
|
||||
* the settings UI replaces the reference's contents without remounting this
|
||||
* plugin, and the next scope resolution is expected to see it.
|
||||
*
|
||||
* @returns {string[]} configured paths, blanks dropped.
|
||||
*/
|
||||
function standingRoots() {
|
||||
const configured = configValue(config.extraRoots)
|
||||
return Array.isArray(configured) ? configured.filter(isPath) : []
|
||||
}
|
||||
|
||||
/**
|
||||
* The scope of one agent, or `undefined` when it has none.
|
||||
@@ -66,7 +118,7 @@ export function createFacetPolicy(config) {
|
||||
function extrasFor(agent, cwd) {
|
||||
const sessionId = typeof agent?.session?.id === 'string' ? agent.session.id : undefined
|
||||
const own = sessionId === undefined ? [] : grants.get(sessionId) ?? []
|
||||
const all = [...standing, ...own].map((path) => (isAbsolute(path) ? resolve(path) : resolve(cwd, path)))
|
||||
const all = [...standingRoots(), ...own].map((path) => (isAbsolute(path) ? resolve(path) : resolve(cwd, path)))
|
||||
return [...new Set(all)]
|
||||
}
|
||||
|
||||
@@ -124,11 +176,17 @@ export function createFacetPolicy(config) {
|
||||
}
|
||||
|
||||
return {
|
||||
mode: config.mode,
|
||||
/** The enforcement mode. A getter, so a settings edit is answered on the next call. */
|
||||
get mode() {
|
||||
return configValue(config.mode)
|
||||
},
|
||||
/** Prompt placements, carried here so the preset row needs no configuration of its own. */
|
||||
sectionOrder: config.sectionOrder ?? 100,
|
||||
contextOrder: config.contextOrder ?? 105,
|
||||
standing: [...standing],
|
||||
sectionOrder: configValue(config.sectionOrder) ?? 100,
|
||||
contextOrder: configValue(config.contextOrder) ?? 105,
|
||||
/** The standing grants. A getter for the same reason as {@link mode}. */
|
||||
get standing() {
|
||||
return standingRoots()
|
||||
},
|
||||
scopeFor,
|
||||
grant,
|
||||
revoke,
|
||||
|
||||
Reference in new issue
Block a user