feat(skills): add per-step no-op execution envelopes

This commit is contained in:
William Valentin
2026-02-12 19:03:27 -08:00
parent 0d324886eb
commit 5e5d96523e
3 changed files with 153 additions and 6 deletions
+107 -4
View File
@@ -19,6 +19,7 @@ import {
renderSkillInstallerExecutionStub,
toSkillInstallerExecutionStubFromPreflight,
evaluateInstallerExecutionPolicy,
toInstallerExecutionStepEnvelopes,
runInstallerCommandsWithPolicy,
noOpSkillInstallerCommandRunner,
runSkillInstallAction,
@@ -238,6 +239,17 @@ describe('skills CLI helpers', () => {
expect(view.execution_enabled).toBe(false);
expect(view.executed).toEqual([]);
expect(view.reason).toBe('execution_disabled');
expect(view.attempted.length).toBe(1);
expect(view.attempted[0]).toEqual({
installer_type: 'download',
command: expect.stringContaining('download https://example.com/tool.tgz'),
});
expect(view.results[0]).toEqual({
installer_type: 'download',
command: expect.stringContaining('download https://example.com/tool.tgz'),
status: 'skipped',
reason: 'execution_disabled',
});
expect(view.wouldRun.length).toBe(1);
expect(view.wouldRun[0]).toContain('download https://example.com/tool.tgz');
});
@@ -251,6 +263,8 @@ describe('skills CLI helpers', () => {
execution_enabled: false,
executed: [],
reason: 'execution_disabled',
attempted: [{ installer_type: 'brew', command: 'brew install jq' }],
results: [{ installer_type: 'brew', command: 'brew install jq', status: 'skipped', reason: 'execution_disabled' }],
wouldRun: ['brew install jq'],
skipped: [{ installerType: 'node', reason: 'neither pnpm nor npm available in PATH' }],
});
@@ -279,9 +293,42 @@ describe('skills CLI helpers', () => {
expect(view.execution_enabled).toBe(false);
expect(view.executed).toEqual([]);
expect(view.reason).toBe('execution_disabled');
expect(view.attempted).toEqual([
{
installer_type: 'download',
command: 'download https://example.com/a.tgz -> /tmp/a.tgz',
},
]);
expect(view.results).toEqual([
{
installer_type: 'download',
command: 'download https://example.com/a.tgz -> /tmp/a.tgz',
status: 'skipped',
reason: 'execution_disabled',
},
]);
expect(view.wouldRun).toEqual(['download https://example.com/a.tgz -> /tmp/a.tgz']);
});
it('builds blocked step envelopes when confirmation is required', () => {
const policy = evaluateInstallerExecutionPolicy({ mode: 'install', confirmed: false });
const envelopes = toInstallerExecutionStepEnvelopes(
[{ installerType: 'brew', command: 'brew install jq' }],
policy,
);
expect(envelopes.attempted).toEqual([{ installer_type: 'brew', command: 'brew install jq' }]);
expect(envelopes.results).toEqual([
{
installer_type: 'brew',
command: 'brew install jq',
status: 'blocked',
reason: 'confirmation_required',
},
]);
});
it('marks install execution policy as confirmation_required when not confirmed', () => {
const policy = evaluateInstallerExecutionPolicy({ mode: 'install', confirmed: false });
@@ -395,7 +442,12 @@ describe('skills CLI helpers', () => {
writeFileSync(join(sourceDir, 'SKILL.md'), '# Plan Skill\nInstructions');
writeFileSync(
join(sourceDir, 'manifest.json'),
JSON.stringify({ name: 'plan-skill', description: 'Plan only', version: '1.0.0' }),
JSON.stringify({
name: 'plan-skill',
description: 'Plan only',
version: '1.0.0',
installers: [{ type: 'download', url: 'https://example.com/plan.tgz' }],
}),
'utf-8',
);
@@ -416,7 +468,12 @@ describe('skills CLI helpers', () => {
writeFileSync(join(sourceDir, 'SKILL.md'), '# Plan Skill\nInstructions');
writeFileSync(
join(sourceDir, 'manifest.json'),
JSON.stringify({ name: 'plan-skill', description: 'Plan only', version: '1.0.0' }),
JSON.stringify({
name: 'plan-skill',
description: 'Plan only',
version: '1.0.0',
installers: [{ type: 'download', url: 'https://example.com/plan.tgz' }],
}),
'utf-8',
);
@@ -433,6 +490,20 @@ describe('skills CLI helpers', () => {
expect(payload.execution.execution_enabled).toBe(false);
expect(payload.execution.executed).toEqual([]);
expect(payload.execution.reason).toBe('execution_disabled');
expect(payload.execution.attempted).toEqual([
{
installer_type: 'download',
command: 'download https://example.com/plan.tgz -> <default destination>',
},
]);
expect(payload.execution.results).toEqual([
{
installer_type: 'download',
command: 'download https://example.com/plan.tgz -> <default destination>',
status: 'skipped',
reason: 'execution_disabled',
},
]);
logSpy.mockRestore();
rmSync(root, { recursive: true, force: true });
@@ -446,7 +517,12 @@ describe('skills CLI helpers', () => {
writeFileSync(join(sourceDir, 'SKILL.md'), '# Install Skill\nInstructions');
writeFileSync(
join(sourceDir, 'manifest.json'),
JSON.stringify({ name: 'install-skill', description: 'Install', version: '1.0.0' }),
JSON.stringify({
name: 'install-skill',
description: 'Install',
version: '1.0.0',
installers: [{ type: 'download', url: 'https://example.com/install.tgz' }],
}),
'utf-8',
);
@@ -463,6 +539,20 @@ describe('skills CLI helpers', () => {
expect(payload.execution.execution_enabled).toBe(false);
expect(payload.execution.executed).toEqual([]);
expect(payload.execution.reason).toBe('confirmation_required');
expect(payload.execution.attempted).toEqual([
{
installer_type: 'download',
command: 'download https://example.com/install.tgz -> <default destination>',
},
]);
expect(payload.execution.results).toEqual([
{
installer_type: 'download',
command: 'download https://example.com/install.tgz -> <default destination>',
status: 'blocked',
reason: 'confirmation_required',
},
]);
logSpy.mockRestore();
rmSync(root, { recursive: true, force: true });
@@ -476,7 +566,12 @@ describe('skills CLI helpers', () => {
writeFileSync(join(sourceDir, 'SKILL.md'), '# Install Skill\nInstructions');
writeFileSync(
join(sourceDir, 'manifest.json'),
JSON.stringify({ name: 'install-skill', description: 'Install', version: '1.0.0' }),
JSON.stringify({
name: 'install-skill',
description: 'Install',
version: '1.0.0',
installers: [{ type: 'download', url: 'https://example.com/install-confirmed.tgz' }],
}),
'utf-8',
);
@@ -490,6 +585,14 @@ describe('skills CLI helpers', () => {
expect(runnerSpy).not.toHaveBeenCalled();
const payload = JSON.parse(String(logSpy.mock.calls[logSpy.mock.calls.length - 1]?.[0]));
expect(payload.execution.executed).toEqual([]);
expect(payload.execution.results).toEqual([
{
installer_type: 'download',
command: 'download https://example.com/install-confirmed.tgz -> <default destination>',
status: 'skipped',
reason: 'execution_disabled',
},
]);
runnerSpy.mockRestore();
logSpy.mockRestore();
+35
View File
@@ -46,6 +46,8 @@ export interface SkillInstallerExecutionStubView {
execution_enabled: boolean;
executed: string[];
reason: SkillInstallerExecutionReason;
attempted: Array<{ installer_type: string; command: string }>;
results: Array<{ installer_type: string; command: string; status: 'blocked' | 'skipped'; reason: SkillInstallerExecutionReason }>;
wouldRun: string[];
skipped: SkillInstallerPlanView['skipped'];
}
@@ -69,6 +71,31 @@ export const noOpSkillInstallerCommandRunner: SkillInstallerCommandRunner = {
},
};
export function toInstallerExecutionStepEnvelopes(
steps: Array<{ installerType: string; command: string }>,
policy: SkillInstallerExecutionPolicy,
): {
attempted: SkillInstallerExecutionStubView['attempted'];
results: SkillInstallerExecutionStubView['results'];
} {
const attempted = steps.map((step) => ({
installer_type: step.installerType,
command: step.command,
}));
const status: SkillInstallerExecutionStubView['results'][number]['status'] =
policy.reason === 'confirmation_required' ? 'blocked' : 'skipped';
const results = attempted.map((step) => ({
installer_type: step.installer_type,
command: step.command,
status,
reason: policy.reason,
}));
return { attempted, results };
}
export function toSkillListRows(skills: Skill[]): SkillListRow[] {
return skills
.map((skill) => ({
@@ -230,6 +257,7 @@ export function renderSkillInstallPreflight(view: SkillInstallPreflightView): st
export function toSkillInstallerExecutionStubView(skill: Skill): SkillInstallerExecutionStubView {
const plan = toSkillInstallerPlanView(skill);
const policy = evaluateInstallerExecutionPolicy({ mode: 'stub', confirmed: false });
const stepEnvelopes = toInstallerExecutionStepEnvelopes(plan.steps, policy);
return {
skill: plan.skill,
execution: 'stub',
@@ -238,6 +266,8 @@ export function toSkillInstallerExecutionStubView(skill: Skill): SkillInstallerE
execution_enabled: policy.execution_enabled,
executed: [],
reason: policy.reason,
attempted: stepEnvelopes.attempted,
results: stepEnvelopes.results,
wouldRun: plan.steps.map((step) => step.command),
skipped: plan.skipped,
};
@@ -249,6 +279,7 @@ export function toSkillInstallerExecutionStubFromPreflight(
): SkillInstallerExecutionStubView {
const mode = options?.mode ?? 'stub';
const policy = evaluateInstallerExecutionPolicy({ mode, confirmed: options?.confirmed ?? false });
const stepEnvelopes = toInstallerExecutionStepEnvelopes(preflight.steps, policy);
return {
skill: preflight.skill,
execution: 'stub',
@@ -257,6 +288,8 @@ export function toSkillInstallerExecutionStubFromPreflight(
execution_enabled: policy.execution_enabled,
executed: [],
reason: policy.reason,
attempted: stepEnvelopes.attempted,
results: stepEnvelopes.results,
wouldRun: preflight.steps.map((step) => step.command),
skipped: preflight.skipped,
};
@@ -451,6 +484,8 @@ export function runSkillInstallAction(
execution_enabled: installPolicy.execution_enabled,
executed: runInstallerCommandsWithPolicy([], installPolicy, opts.commandRunner ?? noOpSkillInstallerCommandRunner),
reason: installPolicy.reason,
attempted: [],
results: [],
wouldRun: [],
skipped: [],
};