The service moved its "is a roster composed" reads to the derived root set; the invariant companion still read `config.roots`. In the shape this change exists for — an app configures nothing and the roster is the harness home alone — that made the advisory warning fire while the fail-loud invariant stayed silent, so an agent could address a model against an empty global layer unchecked. Both now read one source: `roots` exposes the resolved set, and the invariant asks it. That decides the behavior deliberately rather than by omission — a composition that mounts the roster now fails an unjoined agent whether its roots were configured or derived, and `includeUserRoot: false` with no configured roots is how a deployment keeps its agents on the host plane. Both shapes are pinned; the derived-only case fails against the old predicate. Three pieces of prose went stale with the first commit: the web-app bundle comment still called the writable root an assembly fact patched in by AppCLIEntry (removed in the profile-plugin-bundles refactor — `composeProfile` owns it now, and only for the shipped root), and the shipped skill and its Agent Note still called both roots "configuration". The README gains the resolved-roster reader and the discoverable-but-undeletable preset a second writable root produces.
81 lines
3.9 KiB
TypeScript
81 lines
3.9 KiB
TypeScript
/**
|
|
* Package-owned invariant companion for `@deepseek-ai/dsh-agent-presets`.
|
|
* @module @deepseek-ai/dsh-agent-presets/invariant
|
|
*/
|
|
|
|
import type { Context } from '@deepseek-ai/cordis'
|
|
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
|
// Type-only: resolves the `system-prompt/assemble` waterfall this companion
|
|
// joins, and the `agent` field `dsh-agent` merges into its context.
|
|
import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
import type {} from '@deepseek-ai/dsh-agent'
|
|
// Imported through the package name, not `./mount.ts`: a module shared between
|
|
// the two build entry points becomes a third chunk that the published `files`
|
|
// list does not carry, which `verify-built-package-invariants` rejects.
|
|
import { leakedServices, livePresetMounts } from '@deepseek-ai/dsh-agent-presets'
|
|
|
|
const PACKAGE_NAME = '@deepseek-ai/dsh-agent-presets'
|
|
|
|
/** Cordis companion plugin name. */
|
|
export const name = 'agent-presets-invariant'
|
|
/** Service required before the companion can reserve package ownership. */
|
|
export const inject = ['invariants']
|
|
|
|
/**
|
|
* Assert that no installed preset composition reaches the root service realm,
|
|
* and that a deployment configuring a roster composes every agent from it.
|
|
*
|
|
* `mountPreset` proves the first once, when the subtree settles. A row that
|
|
* publishes later — from a timer, or an asynchronous continuation after its
|
|
* plugin returned — would escape that one-shot audit, so re-check every live
|
|
* mount whenever a service registration changes.
|
|
*/
|
|
const install: InvariantInstaller = (ctx, fail) => {
|
|
ctx.on('internal/service', function (this: Context, name) {
|
|
for (const mount of livePresetMounts()) {
|
|
const leaked = leakedServices(ctx, mount.fiber)
|
|
if (leaked.length === 0) continue
|
|
fail(
|
|
`preset "${mount.presetId}" published process-global service(s) [${leaked.join(', ')}] `
|
|
+ `after its mount was audited (observed while notifying "${name}") — `
|
|
+ 'a preset service must sit behind an `isolate` realm or move to the host composition',
|
|
)
|
|
}
|
|
}, { global: true })
|
|
|
|
// An agent that joined no preset resolves `tools`, `system-prompt`, and
|
|
// `skill` against the empty global layer, so the model receives nothing.
|
|
// `composedPreset()` is the roster's own answer to "did this agent join",
|
|
// read from the live scope chain — see the [Agent
|
|
// Note](../../../../.agents/notes/implemented/architecture/2026-08-10-host-plane-ownership-after-presets.md)
|
|
// for why the warning beside it is advisory while this one fails.
|
|
//
|
|
// Two conditions, each load-bearing. `context.agent` is what makes this an
|
|
// AGENT assembly: a scope-only assembly — a cold read resolving presenters
|
|
// in a standing key, a diagnostic — is not an agent and must not be judged
|
|
// on whether it joined anything. And assembly rather than publication is the
|
|
// moment that matters, because an unjoined agent is legal until it addresses
|
|
// a model: `recompose` binds a bare agent as its first link, and that agent
|
|
// is unjoined for its whole life up to the switch.
|
|
ctx.on('system-prompt/assemble', (_assembly, context, next) => {
|
|
const presets = ctx.get('agentPresets')
|
|
const agent = context.agent
|
|
if (presets !== undefined && presets.roots.length > 0
|
|
&& agent !== undefined && presets.composedPreset(agent.ctx) === undefined) {
|
|
fail(
|
|
`agent "${agent.id}" addressed a model without joining any agent preset while a roster is `
|
|
+ 'composed; its tools, prompt sections, and skill catalog resolve against the empty global layer',
|
|
)
|
|
}
|
|
return next()
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Register this package's invariant companion.
|
|
* @param ctx - Cordis context carrying the invariant service.
|
|
* @returns the installed registration's disposer after setup succeeds.
|
|
*/
|
|
export const apply = (ctx: Context): Promise<() => void> =>
|
|
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|