Skip to content

Commit fb905cb

Browse files
fix(config): keep a malformed plugin block the validator's AB4000 when discovery derives the skill identity
Review: discovery dereferenced config.plugin.name before validateSource ran, turning {} or plugin: null into a generic AB7000. declaredPluginIdentity() returns undefined for an unusable plugin.name; the rendered skill then gets no identity (AB3003) and validation reports AB4000 as before.
1 parent 0bf21b7 commit fb905cb

3 files changed

Lines changed: 37 additions & 4 deletions

File tree

‎packages/agent-bundle/src/config/discover.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { compileRouteGraph, isEmptyRouteGraph } from '../routes/graph.ts';
1212
import type { CompiledRouteGraph } from '../routes/types.ts';
1313
import { parseCommand, type CommandDocument } from './command.ts';
1414
import { isProjectPathIgnored, readProjectIgnoreRules } from './ignore.ts';
15-
import { pluginIdentity } from './plugin-identity.ts';
15+
import { declaredPluginIdentity } from './plugin-identity.ts';
1616
import { isRenderedSkillSourceName } from './rendered-skill.ts';
1717
import { parseRule, type RuleDocument } from './rule.ts';
1818
import { parseSkill, type SkillDocument } from './skill.ts';
@@ -257,8 +257,11 @@ export const discoverProject = async (
257257
const rules = await readProjectIgnoreRules(projectRoot);
258258
// Rendered skills evaluate during discovery, before normalization stamps
259259
// the same identity into the model; `agent-bundle/meta` serves it to them
260-
// here so a skill documents the version its plugin ships (#440).
261-
const meta = projectMeta(pluginIdentity(projectRoot, config));
260+
// here so a skill documents the version its plugin ships (#440). A config
261+
// without a usable `plugin.name` is the validator's AB4000, not a crash
262+
// here, and such a skill gets no identity rather than a fabricated one.
263+
const identity = declaredPluginIdentity(projectRoot, config as Readonly<Record<string, unknown>>);
264+
const meta = identity === undefined ? undefined : projectMeta(identity);
262265
const configuredSkills = config.skills;
263266
const conventionalSources = (await fastGlob('src/skills/*/SKILL.{md,ts,tsx}', {
264267
absolute: true,
@@ -352,7 +355,7 @@ export const discoverProject = async (
352355
...(discoveredRules.length === 0 ? {} : { rules: discoveredRules }),
353356
...(shadowedConventionalSkills.length === 0 ? {} : { shadowedConventionalSkills }),
354357
skills: await Promise.all(
355-
skillDirs.map((skillDir) => parseSkill(skillDir, projectRoot, rules, { meta })),
358+
skillDirs.map((skillDir) => parseSkill(skillDir, projectRoot, rules, meta === undefined ? {} : { meta })),
356359
),
357360
...(state === undefined ? {} : { state }),
358361
};

‎packages/agent-bundle/src/config/plugin-identity.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ProjectMetaSource } from '../build/meta.ts';
22
import { developmentFallbackVersion, snapshotPackageIdentity } from '../core/project-context.ts';
3+
import { isRecord } from '../core/strict-json.ts';
34
import type { AgentBundleConfig } from '../core/types.ts';
45

56
/**
@@ -39,3 +40,22 @@ export const pluginIdentity = (
3940
version: resolvePluginVersion(config.plugin.version, packageIdentity.packageVersion),
4041
});
4142
};
43+
44+
/**
45+
* {@link pluginIdentity} for a configuration that has not been validated yet
46+
* (discovery runs before `validateSource`): undefined when `plugin.name` is
47+
* not a nonempty string, so a malformed `plugin` block stays the validator's
48+
* `AB4000` to report rather than a crash here, and no fabricated identity is
49+
* ever served in its place.
50+
*/
51+
export const declaredPluginIdentity = (
52+
projectRoot: string,
53+
config: Readonly<Record<string, unknown>>,
54+
): ProjectMetaSource | undefined => {
55+
const plugin = config.plugin;
56+
if (!isRecord(plugin) || typeof plugin.name !== 'string' || plugin.name.trim().length === 0) return undefined;
57+
// `resolvePluginVersion` already treats a non-string `version` as absent.
58+
return pluginIdentity(projectRoot, {
59+
plugin: { name: plugin.name, ...(typeof plugin.version === 'string' ? { version: plugin.version } : {}) },
60+
});
61+
};

‎packages/agent-bundle/tests/rendered-skills.test.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,16 @@ describe('rendered skill compilation', () => {
280280
// it resolves however the project resolves `agent-bundle` — here, not at all.
281281
const direct = await parseSkill(join(root, 'src', 'skills', 'identity'), root);
282282
expect(direct.diagnostics).toEqual([expect.objectContaining({ code: 'AB3003' })]);
283+
284+
// A config with no usable plugin.name is the validator's AB4000: discovery
285+
// still completes, and the skill is served no fabricated identity.
286+
for (const malformed of [{}, { plugin: null }, { plugin: { name: '' } }, { plugin: 'x' }]) {
287+
const config = malformed as unknown as AgentBundleConfig;
288+
const withoutIdentity = await discoverProject(root, config);
289+
expect(withoutIdentity.skills[0]?.diagnostics).toEqual([expect.objectContaining({ code: 'AB3003' })]);
290+
expect(validateSource(loadedProject(config, root), withoutIdentity, registry).map(({ code }) => code))
291+
.toContain('AB4000');
292+
}
283293
});
284294

285295
it('compiles JSX against the loader element factory, not the consumer react/jsx-runtime (#441)', async () => {

0 commit comments

Comments
 (0)