agent-skillboard 0.2.18 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/README.md +129 -258
- package/docs/adapters.md +37 -113
- package/docs/ai-skill-routing-goal.md +35 -109
- package/docs/capabilities.md +17 -104
- package/docs/install.md +36 -485
- package/docs/policy-model.md +50 -280
- package/docs/positioning.md +19 -86
- package/docs/profiles.md +21 -153
- package/docs/reference.md +115 -363
- package/docs/rollout-runbook.md +23 -25
- package/docs/routing.md +23 -90
- package/docs/user-flow.md +60 -284
- package/docs/value-proof.md +23 -181
- package/docs/variant-lifecycle.md +47 -67
- package/docs/versioning.md +31 -264
- package/examples/v2-multi-source.config.yaml +35 -0
- package/examples/v2-policy-error.config.yaml +6 -0
- package/package.json +1 -1
- package/src/advisor/actions.mjs +102 -6
- package/src/advisor/application-commands.mjs +10 -9
- package/src/advisor/apply-action.mjs +74 -1
- package/src/advisor/guidance.mjs +24 -16
- package/src/advisor/schema.mjs +17 -6
- package/src/advisor/skills.mjs +18 -5
- package/src/advisor.mjs +27 -9
- package/src/agent-integration-cli.mjs +13 -4
- package/src/agent-integration-content.mjs +21 -11
- package/src/agent-integration-files.mjs +1 -1
- package/src/agent-inventory-platforms.mjs +10 -0
- package/src/agent-inventory.mjs +23 -1
- package/src/agent-skill-import.mjs +2 -2
- package/src/audit-paths.mjs +42 -0
- package/src/brief-cli.mjs +3 -2
- package/src/brief-renderer.mjs +1 -0
- package/src/cli.mjs +498 -232
- package/src/compatibility.mjs +24 -0
- package/src/control/can-use-guard.mjs +21 -1
- package/src/control/config-write.mjs +32 -2
- package/src/control/skill-crud.mjs +5 -0
- package/src/control/v2-guard.mjs +175 -0
- package/src/control/v2-skill-crud.mjs +32 -0
- package/src/control/v2-skill-forget.mjs +38 -0
- package/src/control/variant-status.mjs +47 -1
- package/src/control.mjs +55 -0
- package/src/doctor.mjs +63 -4
- package/src/domain/v2-policy.mjs +111 -0
- package/src/hook-plan.mjs +33 -3
- package/src/impact.mjs +52 -29
- package/src/index.mjs +25 -1
- package/src/init.mjs +50 -34
- package/src/inventory-install-units.mjs +63 -0
- package/src/inventory-json.mjs +279 -0
- package/src/inventory-refresh.mjs +163 -18
- package/src/lifecycle-cli.mjs +40 -12
- package/src/lifecycle-content.mjs +52 -67
- package/src/migration/v1-to-v2.mjs +212 -0
- package/src/migration/v2-files.mjs +211 -0
- package/src/migration/v2-journal.mjs +169 -0
- package/src/migration/v2-projection.mjs +108 -0
- package/src/migration/v2-transaction.mjs +205 -0
- package/src/policy.mjs +3 -0
- package/src/reconcile.mjs +139 -111
- package/src/report.mjs +168 -148
- package/src/review.mjs +2 -0
- package/src/route-advisory.mjs +47 -2
- package/src/route-selection.mjs +38 -2
- package/src/route.mjs +62 -2
- package/src/shared-skill.mjs +301 -0
- package/src/source-digest.mjs +42 -0
- package/src/source-profiles.mjs +171 -144
- package/src/source-verification.mjs +32 -48
- package/src/uninstall.mjs +22 -0
- package/src/user-state-paths.mjs +19 -0
- package/src/user-uninstall.mjs +146 -0
- package/src/workspace.mjs +119 -79
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const V1_COMPATIBILITY_REMOVAL_VERSION = "v0.4.0";
|
|
2
|
+
export const V1_MIGRATION_COMMAND = "skillboard migrate v2";
|
|
3
|
+
export const V1_COMPATIBILITY_NOTICE =
|
|
4
|
+
`Version 1 policy is deprecated and read-only; run \`${V1_MIGRATION_COMMAND}\`. Support ends in package release ${V1_COMPATIBILITY_REMOVAL_VERSION}.`;
|
|
5
|
+
export const V1_MUTATION_ERROR = `Version 1 policy is read-only. Run \`${V1_MIGRATION_COMMAND}\`.`;
|
|
6
|
+
export const STALE_V1_PROJECTION_ERROR = "This pre-v2 policy projection is stale; regenerate it from version 2 policy.";
|
|
7
|
+
|
|
8
|
+
export function compatibilityForVersion(version) {
|
|
9
|
+
return version === 1 ? { notice: V1_COMPATIBILITY_NOTICE, removalVersion: V1_COMPATIBILITY_REMOVAL_VERSION } : null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function assertV2MutationVersion(version) {
|
|
13
|
+
if (version === 1 || version === undefined) throw new Error(V1_MUTATION_ERROR);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isPreV2ActionId(actionId) {
|
|
17
|
+
return /^(?:review-install-unit|trust-install-unit|activate-skill|block-install-unit):/.test(actionId);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function assertCurrentProjectionVersion(projectionVersion, policyVersion) {
|
|
21
|
+
if (projectionVersion !== undefined && Number(projectionVersion) !== policyVersion) {
|
|
22
|
+
throw new Error(`${STALE_V1_PROJECTION_ERROR} Expected policy projection version ${policyVersion}, got ${projectionVersion}.`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -6,8 +6,12 @@ import {
|
|
|
6
6
|
import { workflowConflictEntriesForSkill } from "../conflicts.mjs";
|
|
7
7
|
import { isModelSelectableInvocation, isUserControlledSource } from "../domain/source-classes.mjs";
|
|
8
8
|
import { classifySkillTrust, workflowSkillRole } from "./source-trust.mjs";
|
|
9
|
+
import { authorizeV2Skill } from "./v2-guard.mjs";
|
|
9
10
|
|
|
10
|
-
export function canUseSkill(workspace, skillId, workflowName) {
|
|
11
|
+
export function canUseSkill(workspace, skillId, workflowName, agentName) {
|
|
12
|
+
if (workspace.version === 2) {
|
|
13
|
+
return canUseV2Skill(workspace, skillId, agentName);
|
|
14
|
+
}
|
|
11
15
|
const policy = checkPolicy(workspace);
|
|
12
16
|
const skill = workspace.skills.find((candidate) => candidate.id === skillId);
|
|
13
17
|
const workflow = workspace.workflows.find((candidate) => candidate.name === workflowName);
|
|
@@ -51,6 +55,22 @@ export function canUseSkill(workspace, skillId, workflowName) {
|
|
|
51
55
|
return canUseResult(reasons.length === 0, skillId, workflowName, skill, workflow, reasons, role, classifySkillTrust(workspace, skill));
|
|
52
56
|
}
|
|
53
57
|
|
|
58
|
+
function canUseV2Skill(workspace, skillId, agentName) {
|
|
59
|
+
const skill = workspace.skills.find((candidate) => candidate.id === skillId);
|
|
60
|
+
const authorization = authorizeV2Skill(skillId, skill, workspace.inventory, agentName);
|
|
61
|
+
return {
|
|
62
|
+
allowed: authorization.allowed,
|
|
63
|
+
automaticAllowed: authorization.allowed,
|
|
64
|
+
allowedUse: authorization.allowed ? allowedUse(skillId) : null,
|
|
65
|
+
skill: skillId,
|
|
66
|
+
workflow: null,
|
|
67
|
+
agent: agentName ?? null,
|
|
68
|
+
workflowKnown: true,
|
|
69
|
+
integrityError: authorization.integrityError,
|
|
70
|
+
reasons: authorization.reasons
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
54
74
|
function trustUseReasons(workspace, skill) {
|
|
55
75
|
const unit = skill.ownerInstallUnit === undefined
|
|
56
76
|
? undefined
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
1
|
+
import { open, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { basename, dirname, join } from "node:path";
|
|
4
4
|
import YAML from "yaml";
|
|
@@ -6,6 +6,7 @@ import { loadWorkspace } from "../workspace.mjs";
|
|
|
6
6
|
import { checkPolicy } from "../policy.mjs";
|
|
7
7
|
import { textChangePlan } from "../change-plan.mjs";
|
|
8
8
|
import { canUseSkill } from "./can-use-guard.mjs";
|
|
9
|
+
import { assertV2MutationVersion } from "../compatibility.mjs";
|
|
9
10
|
|
|
10
11
|
export async function loadConfig(path) {
|
|
11
12
|
const text = await readFile(path, "utf8");
|
|
@@ -21,10 +22,12 @@ export async function loadConfig(path) {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export async function writeCheckedConfig(document, originalText, options, message) {
|
|
25
|
+
assertV2MutationVersion(document.get("version") ?? 1);
|
|
24
26
|
const nextText = preserveLineEndings(String(document), originalText);
|
|
25
27
|
const plan = textChangePlan(originalText, nextText);
|
|
26
28
|
const tempPath = tempConfigPath(options.configPath);
|
|
27
|
-
|
|
29
|
+
const originalMode = (await stat(options.configPath)).mode & 0o777;
|
|
30
|
+
await writeFile(tempPath, nextText, { encoding: "utf8", flag: "wx", mode: originalMode });
|
|
28
31
|
try {
|
|
29
32
|
const workspace = await loadWorkspace({ configPath: tempPath, skillsRoot: options.skillsRoot });
|
|
30
33
|
const policy = checkPolicy(workspace);
|
|
@@ -50,6 +53,33 @@ export async function writeCheckedConfig(document, originalText, options, messag
|
|
|
50
53
|
}
|
|
51
54
|
}
|
|
52
55
|
|
|
56
|
+
export async function withPolicyMutationLock(configPath, operation) {
|
|
57
|
+
const canonicalPath = await realpath(configPath);
|
|
58
|
+
const lockPath = `${canonicalPath}.migrate.lock`;
|
|
59
|
+
const deadline = Date.now() + 5_000;
|
|
60
|
+
let handle;
|
|
61
|
+
while (handle === undefined) {
|
|
62
|
+
try {
|
|
63
|
+
handle = await open(lockPath, "wx", 0o600);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error?.code !== "EEXIST" || Date.now() >= deadline) {
|
|
66
|
+
throw error?.code === "EEXIST"
|
|
67
|
+
? new Error("Another policy update is already using this config.")
|
|
68
|
+
: error;
|
|
69
|
+
}
|
|
70
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
await handle.writeFile(`${process.pid}\n`);
|
|
75
|
+
await handle.sync();
|
|
76
|
+
return await operation(canonicalPath);
|
|
77
|
+
} finally {
|
|
78
|
+
await handle.close();
|
|
79
|
+
await rm(lockPath, { force: true });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
53
83
|
function tempConfigPath(configPath) {
|
|
54
84
|
return join(dirname(configPath), `.${basename(configPath)}.${randomUUID()}.tmp`);
|
|
55
85
|
}
|
|
@@ -26,12 +26,14 @@ import {
|
|
|
26
26
|
writeCheckedConfig
|
|
27
27
|
} from "./config-write.mjs";
|
|
28
28
|
import { canUseSkill } from "./can-use-guard.mjs";
|
|
29
|
+
import { assertV2MutationVersion } from "../compatibility.mjs";
|
|
29
30
|
|
|
30
31
|
const WRITABLE_INVOCATIONS = new Set(["manual-only", "router-only", "workflow-auto", "global-auto"]);
|
|
31
32
|
const NON_ACTIVATABLE_STATUSES = new Set(["blocked", "deprecated", "archived", "removed"]);
|
|
32
33
|
|
|
33
34
|
export async function activateSkill(options) {
|
|
34
35
|
const { document, originalText } = await loadConfig(options.configPath);
|
|
36
|
+
assertV2MutationVersion(document.get("version") ?? 1);
|
|
35
37
|
const skill = requireConfigSkill(document, options.skillId);
|
|
36
38
|
const workflow = requireConfigWorkflow(document, options.workflow);
|
|
37
39
|
const mode = options.mode ?? readMapString(skill, "invocation", "manual-only");
|
|
@@ -55,6 +57,7 @@ export async function activateSkill(options) {
|
|
|
55
57
|
|
|
56
58
|
export async function addSkill(options) {
|
|
57
59
|
const { document, originalText } = await loadConfig(options.configPath);
|
|
60
|
+
assertV2MutationVersion(document.get("version") ?? 1);
|
|
58
61
|
const skills = ensureMapAt(document, ["skills"], "skills");
|
|
59
62
|
if (skills.get(options.skillId, true) !== undefined) {
|
|
60
63
|
throw new Error(`Skill already exists: ${options.skillId}`);
|
|
@@ -147,6 +150,7 @@ export async function addSkillVariant(options) {
|
|
|
147
150
|
|
|
148
151
|
export async function blockSkill(options) {
|
|
149
152
|
const { document, originalText } = await loadConfig(options.configPath);
|
|
153
|
+
assertV2MutationVersion(document.get("version") ?? 1);
|
|
150
154
|
requireConfigSkill(document, options.skillId);
|
|
151
155
|
const workflow = requireConfigWorkflow(document, options.workflow);
|
|
152
156
|
|
|
@@ -188,6 +192,7 @@ export async function removeSkill(options) {
|
|
|
188
192
|
|
|
189
193
|
export async function preferSkill(options) {
|
|
190
194
|
const { document, originalText } = await loadConfig(options.configPath);
|
|
195
|
+
assertV2MutationVersion(document.get("version") ?? 1);
|
|
191
196
|
const skill = requireConfigSkill(document, options.skillId);
|
|
192
197
|
const workflow = requireConfigWorkflow(document, options.workflow);
|
|
193
198
|
const capabilities = requireMapAt(document, ["capabilities"], "capabilities");
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { supportedAgentNames } from "../agent-skill-roots.mjs";
|
|
3
|
+
|
|
4
|
+
export async function loadV2InventoryIndex(path) {
|
|
5
|
+
const text = await readFile(path, "utf8").catch((error) => {
|
|
6
|
+
if (error?.code === "ENOENT") {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
throw error;
|
|
10
|
+
});
|
|
11
|
+
if (text === null) {
|
|
12
|
+
return { path, integrityErrors: [`generated inventory is missing: ${path}`], skillIds: [], skills: [], installUnits: [] };
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const value = JSON.parse(text);
|
|
16
|
+
const errors = validateInventory(value);
|
|
17
|
+
const skillIds = errors.length === 0
|
|
18
|
+
? value.skills.map((skill) => skill.id).sort((left, right) => left.localeCompare(right))
|
|
19
|
+
: [];
|
|
20
|
+
const installUnits = errors.length === 0
|
|
21
|
+
? (value.install_units ?? []).map(projectInstallUnitObservation)
|
|
22
|
+
: [];
|
|
23
|
+
return { path, integrityErrors: errors, skillIds, skills: errors.length === 0 ? value.skills : [], installUnits };
|
|
24
|
+
} catch (error) {
|
|
25
|
+
return { path, integrityErrors: [`generated inventory is invalid JSON: ${errorMessage(error)}`], skillIds: [], skills: [], installUnits: [] };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function authorizeV2Skill(skillId, policy, inventory, agentName) {
|
|
30
|
+
if (typeof agentName !== "string" || agentName.length === 0) {
|
|
31
|
+
return {
|
|
32
|
+
allowed: false,
|
|
33
|
+
integrityError: false,
|
|
34
|
+
reasons: ["Current agent is required for version 2 availability checks."]
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (!supportedAgentNames().includes(agentName)) {
|
|
38
|
+
return {
|
|
39
|
+
allowed: false,
|
|
40
|
+
integrityError: false,
|
|
41
|
+
reasons: [`Unsupported agent: ${agentName}.`]
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const integrityReasons = inventoryIntegrityReasons(inventory, skillId);
|
|
45
|
+
if (integrityReasons.length > 0) {
|
|
46
|
+
return { allowed: false, integrityError: true, reasons: integrityReasons };
|
|
47
|
+
}
|
|
48
|
+
if (policy === undefined) {
|
|
49
|
+
return { allowed: false, integrityError: false, reasons: ["Skill has no version 2 policy entry."] };
|
|
50
|
+
}
|
|
51
|
+
if (!policy.enabled) {
|
|
52
|
+
return { allowed: false, integrityError: false, reasons: [`Skill ${policy.id} is disabled.`] };
|
|
53
|
+
}
|
|
54
|
+
const observation = inventory.skills?.find((skill) => skill.id === skillId);
|
|
55
|
+
const installedOn = Array.isArray(observation?.installed_on) ? observation.installed_on : [];
|
|
56
|
+
if (!installedOn.includes(agentName)) {
|
|
57
|
+
return {
|
|
58
|
+
allowed: false,
|
|
59
|
+
integrityError: false,
|
|
60
|
+
reasons: [`Skill ${policy.id} is not installed for agent ${agentName}.`]
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return { allowed: true, integrityError: false, reasons: [] };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function validateInventory(value) {
|
|
67
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
68
|
+
return ["generated inventory root must be an object"];
|
|
69
|
+
}
|
|
70
|
+
const errors = [];
|
|
71
|
+
if (value.format_version !== 1) errors.push("generated inventory format_version must be 1");
|
|
72
|
+
if (value.generated !== true) errors.push("generated inventory must declare generated: true");
|
|
73
|
+
if (value.authoritative_for_availability !== false) {
|
|
74
|
+
errors.push("generated inventory must declare authoritative_for_availability: false");
|
|
75
|
+
}
|
|
76
|
+
if (!Array.isArray(value.skills)) {
|
|
77
|
+
errors.push("generated inventory skills must be an array");
|
|
78
|
+
return errors;
|
|
79
|
+
}
|
|
80
|
+
const ids = new Set();
|
|
81
|
+
for (const [index, skill] of value.skills.entries()) {
|
|
82
|
+
if (skill === null || typeof skill !== "object" || Array.isArray(skill)) {
|
|
83
|
+
errors.push(`generated inventory skill ${index} must be an object`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
for (const field of ["id", "path", "owner_install_unit"]) {
|
|
87
|
+
if (typeof skill[field] !== "string" || skill[field].trim() === "") {
|
|
88
|
+
errors.push(`generated inventory skill ${index}.${field} must be a non-empty string`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (skill.installed_on !== undefined && (!Array.isArray(skill.installed_on)
|
|
92
|
+
|| skill.installed_on.some((agent) => typeof agent !== "string" || agent.trim() === ""))) {
|
|
93
|
+
errors.push(`generated inventory skill ${index}.installed_on must be a list of agent names`);
|
|
94
|
+
}
|
|
95
|
+
if (typeof skill.id === "string") {
|
|
96
|
+
if (ids.has(skill.id)) errors.push(`generated inventory contains duplicate skill id: ${skill.id}`);
|
|
97
|
+
ids.add(skill.id);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (value.install_units !== undefined && !Array.isArray(value.install_units)) {
|
|
101
|
+
errors.push("generated inventory install_units must be an array");
|
|
102
|
+
return errors;
|
|
103
|
+
}
|
|
104
|
+
const unitIds = new Set();
|
|
105
|
+
for (const [index, unit] of (value.install_units ?? []).entries()) {
|
|
106
|
+
if (unit === null || typeof unit !== "object" || Array.isArray(unit)) {
|
|
107
|
+
errors.push(`generated inventory install unit ${index} must be an object`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (typeof unit.id !== "string" || unit.id.trim() === "") {
|
|
111
|
+
errors.push(`generated inventory install unit ${index}.id must be a non-empty string`);
|
|
112
|
+
} else if (unitIds.has(unit.id)) {
|
|
113
|
+
errors.push(`generated inventory contains duplicate install unit id: ${unit.id}`);
|
|
114
|
+
} else {
|
|
115
|
+
unitIds.add(unit.id);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return errors;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function projectInstallUnitObservation(unit) {
|
|
122
|
+
const runtime = unit.runtime_components ?? {};
|
|
123
|
+
return {
|
|
124
|
+
id: unit.id,
|
|
125
|
+
kind: unit.kind ?? "skill",
|
|
126
|
+
sourceClass: unit.source_class,
|
|
127
|
+
priority: undefined,
|
|
128
|
+
trustLevel: unit.trust_observation ?? "unreviewed",
|
|
129
|
+
sourceDigest: unit.source_digest,
|
|
130
|
+
signature: unit.signature_observed === true ? "observed" : undefined,
|
|
131
|
+
publicKey: undefined,
|
|
132
|
+
verifiedAt: unit.verified_at,
|
|
133
|
+
source: unit.source ?? "",
|
|
134
|
+
scope: "inventory-observation",
|
|
135
|
+
manifestPath: unit.manifest_path ?? "",
|
|
136
|
+
cachePath: unit.cache_path ?? "",
|
|
137
|
+
providedComponents: [],
|
|
138
|
+
components: {
|
|
139
|
+
skills: stringList(unit.skills),
|
|
140
|
+
commands: stringList(runtime.commands),
|
|
141
|
+
hooks: stringList(runtime.hooks),
|
|
142
|
+
mcpServers: stringList(runtime.mcp_servers)
|
|
143
|
+
},
|
|
144
|
+
modifiedConfigFiles: [],
|
|
145
|
+
autoUpdate: false,
|
|
146
|
+
enabled: true,
|
|
147
|
+
workflowDependencies: [],
|
|
148
|
+
permissionRisk: unit.permission_risk ?? "unknown",
|
|
149
|
+
rollback: "observation-only"
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function stringList(value) {
|
|
154
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function errorMessage(error) {
|
|
158
|
+
return error instanceof Error ? error.message : String(error);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function inventoryIntegrityReasons(inventory, skillId) {
|
|
162
|
+
if (inventory === null || typeof inventory !== "object") {
|
|
163
|
+
return ["Inventory integrity error: generated inventory is unavailable."];
|
|
164
|
+
}
|
|
165
|
+
if (Array.isArray(inventory.integrityErrors) && inventory.integrityErrors.length > 0) {
|
|
166
|
+
return inventory.integrityErrors.map((reason) => `Inventory integrity error: ${reason}`);
|
|
167
|
+
}
|
|
168
|
+
const ids = inventory.skillIds instanceof Set
|
|
169
|
+
? inventory.skillIds
|
|
170
|
+
: new Set(Array.isArray(inventory.skillIds) ? inventory.skillIds : []);
|
|
171
|
+
if (!ids.has(skillId)) {
|
|
172
|
+
return [`Inventory integrity error: skill ${skillId ?? "<unknown>"} is missing from generated inventory.`];
|
|
173
|
+
}
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ensureMapAt, loadConfig, requireYamlMap, withPolicyMutationLock, writeCheckedConfig } from "./config-write.mjs";
|
|
2
|
+
|
|
3
|
+
export async function setV2SkillEnabled(options) {
|
|
4
|
+
return mutate(options, (skill) => skill.set("enabled", options.enabled), `${options.enabled ? "Enabled" : "Disabled"} ${options.skillId}`);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function setV2SkillShared(options) {
|
|
8
|
+
return mutate(options, (skill) => skill.set("shared", options.shared), `${options.shared ? "Shared" : "Unshared"} ${options.skillId}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function setV2SkillPreference(options) {
|
|
12
|
+
const intents = names(options.intents, "--intent");
|
|
13
|
+
if (!Number.isInteger(options.priority)) throw new Error("--priority must be an integer");
|
|
14
|
+
return mutate(options, (skill, document) => skill.set("preference", document.createNode({ intents, priority: options.priority })), `Updated preference for ${options.skillId}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function mutate(options, update, message) {
|
|
18
|
+
return await withPolicyMutationLock(options.configPath, async (configPath) => {
|
|
19
|
+
const { document, originalText } = await loadConfig(configPath);
|
|
20
|
+
const skills = ensureMapAt(document, ["skills"], "skills");
|
|
21
|
+
const raw = skills.get(options.skillId, true);
|
|
22
|
+
if (raw === undefined) throw new Error(`Unknown skill: ${options.skillId}`);
|
|
23
|
+
update(requireYamlMap(raw, `skills.${options.skillId}`), document);
|
|
24
|
+
return await writeCheckedConfig(document, originalText, { ...options, configPath }, message);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function names(values, option) {
|
|
29
|
+
const result = [...new Set((values ?? []).map((value) => value.trim()).filter(Boolean))].sort();
|
|
30
|
+
if (result.length === 0) throw new Error(`${option} requires at least one value`);
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { loadWorkspace } from "../workspace.mjs";
|
|
2
|
+
import {
|
|
3
|
+
loadConfig,
|
|
4
|
+
requireYamlMap,
|
|
5
|
+
withPolicyMutationLock,
|
|
6
|
+
writeCheckedConfig
|
|
7
|
+
} from "./config-write.mjs";
|
|
8
|
+
|
|
9
|
+
export async function forgetV2Skill(options) {
|
|
10
|
+
return await withPolicyMutationLock(options.configPath, async (configPath) => {
|
|
11
|
+
const workspace = await loadWorkspace({
|
|
12
|
+
configPath,
|
|
13
|
+
inventoryPath: options.inventoryPath,
|
|
14
|
+
skillsRoot: options.skillsRoot
|
|
15
|
+
});
|
|
16
|
+
const policy = workspace.skills.find((skill) => skill.id === options.skillId);
|
|
17
|
+
if (policy === undefined) throw new Error(`Unknown skill: ${options.skillId}`);
|
|
18
|
+
if (workspace.inventory.integrityErrors.length > 0) {
|
|
19
|
+
throw new Error(`Cannot forget a skill while generated inventory is unhealthy: ${workspace.inventory.integrityErrors.join("; ")}`);
|
|
20
|
+
}
|
|
21
|
+
if (policy.shared) {
|
|
22
|
+
throw new Error(`Skill ${options.skillId} is shared. Run skillboard skill unshare ${options.skillId} before forgetting it.`);
|
|
23
|
+
}
|
|
24
|
+
if (workspace.inventory.skills.some((skill) => skill.id === options.skillId)) {
|
|
25
|
+
throw new Error(`Skill ${options.skillId} is still installed. Remove it from its owning agent before forgetting its policy.`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { document, originalText } = await loadConfig(configPath);
|
|
29
|
+
const skills = requireYamlMap(document.get("skills", true), "skills");
|
|
30
|
+
skills.delete(options.skillId);
|
|
31
|
+
return await writeCheckedConfig(
|
|
32
|
+
document,
|
|
33
|
+
originalText,
|
|
34
|
+
{ ...options, configPath },
|
|
35
|
+
`Forgot ${options.skillId}`
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -7,7 +7,10 @@ import {
|
|
|
7
7
|
|
|
8
8
|
export async function variantLifecycleStatus(options) {
|
|
9
9
|
const workspace = await loadWorkspace({ configPath: options.configPath, skillsRoot: options.skillsRoot });
|
|
10
|
-
const
|
|
10
|
+
const policySkill = workspace.skills.find((candidate) => candidate.id === options.variantId);
|
|
11
|
+
const skill = workspace.version === 2
|
|
12
|
+
? historicalVariantSkill(workspace, policySkill)
|
|
13
|
+
: policySkill;
|
|
11
14
|
if (skill === undefined) {
|
|
12
15
|
throw new Error(`Unknown skill: ${options.variantId}`);
|
|
13
16
|
}
|
|
@@ -39,6 +42,49 @@ export async function variantLifecycleStatus(options) {
|
|
|
39
42
|
};
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
function historicalVariantSkill(workspace, policySkill) {
|
|
46
|
+
if (policySkill === undefined) return undefined;
|
|
47
|
+
const observed = workspace.inventory?.skills?.find((candidate) => candidate.id === policySkill.id);
|
|
48
|
+
const rawVariant = observed?.observations?.variant;
|
|
49
|
+
if (observed === undefined || rawVariant === undefined) return policySkill;
|
|
50
|
+
return {
|
|
51
|
+
...policySkill,
|
|
52
|
+
path: observed.path,
|
|
53
|
+
variant: parseHistoricalVariant(rawVariant, policySkill.id)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseHistoricalVariant(raw, skillId) {
|
|
58
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
59
|
+
throw new Error(`Inventory variant observation for ${skillId} must be an object`);
|
|
60
|
+
}
|
|
61
|
+
const approved = raw.approved === undefined ? undefined : historicalCheckpoint(raw.approved, `${skillId}.approved`);
|
|
62
|
+
return {
|
|
63
|
+
of: requiredString(raw.of, `${skillId}.variant.of`),
|
|
64
|
+
adaptedFor: typeof raw.adapted_for === "string" ? raw.adapted_for : null,
|
|
65
|
+
capability: requiredString(raw.capability, `${skillId}.variant.capability`),
|
|
66
|
+
workflow: requiredString(raw.workflow, `${skillId}.variant.workflow`),
|
|
67
|
+
status: requiredString(raw.status, `${skillId}.variant.status`),
|
|
68
|
+
base: historicalCheckpoint(raw.base, `${skillId}.variant.base`),
|
|
69
|
+
...(approved === undefined ? {} : { approved })
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function historicalCheckpoint(raw, label) {
|
|
74
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
75
|
+
throw new Error(`Inventory variant checkpoint ${label} must be an object`);
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
contentDigest: requiredString(raw.content_digest, `${label}.content_digest`),
|
|
79
|
+
snapshot: requiredString(raw.snapshot, `${label}.snapshot`)
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function requiredString(value, label) {
|
|
84
|
+
if (typeof value !== "string" || value.length === 0) throw new Error(`${label} must be a non-empty string`);
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
|
|
42
88
|
function computeStatus(lifecycleStatus, liveDigest, baseDigest, approvedDigest) {
|
|
43
89
|
if (liveDigest === null) {
|
|
44
90
|
return "missing-live-file";
|
package/src/control.mjs
CHANGED
|
@@ -29,14 +29,22 @@ import { variantLifecycleStatus } from "./control/variant-status.mjs";
|
|
|
29
29
|
import { addHarness, addWorkflow } from "./control/workflow-crud.mjs";
|
|
30
30
|
import {
|
|
31
31
|
GUARD_HOOK_MODE,
|
|
32
|
+
assertHookTargetContained,
|
|
32
33
|
assertGuardHookPlanIsInstallable,
|
|
33
34
|
buildGuardHookInstallPlan,
|
|
34
35
|
planGuardHookInstall
|
|
35
36
|
} from "./hook-plan.mjs";
|
|
36
37
|
|
|
37
38
|
export { planGuardHookInstall } from "./hook-plan.mjs";
|
|
39
|
+
export { setV2SkillEnabled, setV2SkillPreference, setV2SkillShared } from "./control/v2-skill-crud.mjs";
|
|
40
|
+
export { forgetV2Skill } from "./control/v2-skill-forget.mjs";
|
|
38
41
|
|
|
39
42
|
export function listSkills(workspace, options = {}) {
|
|
43
|
+
if (workspace.version === 2) {
|
|
44
|
+
return workspace.skills
|
|
45
|
+
.map((skill) => v2SkillSummary(workspace, skill))
|
|
46
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
47
|
+
}
|
|
40
48
|
const workflow = options.workflow === undefined ? undefined : workflowByName(workspace, options.workflow);
|
|
41
49
|
const skillIds = workflow === undefined
|
|
42
50
|
? new Set(workspace.skills.map((skill) => skill.id))
|
|
@@ -57,6 +65,9 @@ export function listSkills(workspace, options = {}) {
|
|
|
57
65
|
}
|
|
58
66
|
|
|
59
67
|
export function listWorkflows(workspace) {
|
|
68
|
+
if (workspace.version === 2) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
60
71
|
return workspace.workflows.map((workflow) => ({
|
|
61
72
|
name: workflow.name,
|
|
62
73
|
harness: workflow.harness,
|
|
@@ -96,6 +107,12 @@ export function listInstallUnits(workspace) {
|
|
|
96
107
|
|
|
97
108
|
export function explainSkill(workspace, skillId) {
|
|
98
109
|
const skill = skillById(workspace, skillId);
|
|
110
|
+
if (workspace.version === 2) {
|
|
111
|
+
return {
|
|
112
|
+
...v2SkillSummary(workspace, skill),
|
|
113
|
+
agents: v2InventoryObservation(workspace, skill.id).installed_on
|
|
114
|
+
};
|
|
115
|
+
}
|
|
99
116
|
const source = classifySkillSource(workspace, skill);
|
|
100
117
|
const workflows = workspace.workflows
|
|
101
118
|
.map((workflow) => workflowSkillRole(workspace, workflow, skillId))
|
|
@@ -119,6 +136,43 @@ export function explainSkill(workspace, skillId) {
|
|
|
119
136
|
};
|
|
120
137
|
}
|
|
121
138
|
|
|
139
|
+
function v2SkillSummary(workspace, skill) {
|
|
140
|
+
const inventory = v2InventoryObservation(workspace, skill.id);
|
|
141
|
+
return {
|
|
142
|
+
id: skill.id,
|
|
143
|
+
enabled: skill.enabled,
|
|
144
|
+
shared: skill.shared,
|
|
145
|
+
preference: skill.preference,
|
|
146
|
+
path: inventory.path,
|
|
147
|
+
inventory,
|
|
148
|
+
variant: inventory.observations.variant ?? null
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function v2InventoryObservation(workspace, skillId) {
|
|
153
|
+
const observed = workspace.inventory?.skills?.find((candidate) => candidate.id === skillId);
|
|
154
|
+
if (observed === undefined) {
|
|
155
|
+
return {
|
|
156
|
+
present: false, path: null, owner_install_unit: null, source: null,
|
|
157
|
+
category: null, description: null, content_digest: null, installed_on: [], aliases: [], observations: {}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
present: true,
|
|
162
|
+
path: observed.path,
|
|
163
|
+
owner_install_unit: observed.owner_install_unit,
|
|
164
|
+
source: observed.source ?? null,
|
|
165
|
+
category: observed.category ?? null,
|
|
166
|
+
description: observed.description ?? null,
|
|
167
|
+
content_digest: observed.content_digest ?? null,
|
|
168
|
+
installed_on: Array.isArray(observed.installed_on) ? observed.installed_on : [],
|
|
169
|
+
aliases: Array.isArray(observed.aliases) ? observed.aliases : [],
|
|
170
|
+
observations: observed.observations !== null && typeof observed.observations === "object" && !Array.isArray(observed.observations)
|
|
171
|
+
? observed.observations
|
|
172
|
+
: {}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
122
176
|
export {
|
|
123
177
|
activateSkill,
|
|
124
178
|
addHarness,
|
|
@@ -143,6 +197,7 @@ export async function installGuardHook(options) {
|
|
|
143
197
|
const { plan, script } = await buildGuardHookInstallPlan(options);
|
|
144
198
|
assertGuardHookPlanIsInstallable(plan);
|
|
145
199
|
|
|
200
|
+
await assertHookTargetContained(options.configPath, plan.path);
|
|
146
201
|
await mkdir(dirname(plan.path), { recursive: true });
|
|
147
202
|
await assertNewNonSymlinkPath(plan.path);
|
|
148
203
|
await writeFile(plan.path, script, { encoding: "utf8", flag: "wx", mode: GUARD_HOOK_MODE });
|