@aicf/agent-skills 1.0.0-rc.4
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/.codex-plugin/plugin.json +40 -0
- package/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +114 -0
- package/assets/aicf-agent-skills-icon.svg +7 -0
- package/assets/aicf-agent-skills-logo.svg +9 -0
- package/docs/codex-plugin-distribution.md +50 -0
- package/docs/installation.md +96 -0
- package/docs/public-release-checklist.md +57 -0
- package/docs/skill-authoring-standard.md +55 -0
- package/docs/skill-index.md +157 -0
- package/docs/validation.md +50 -0
- package/package.json +61 -0
- package/scripts/README.md +6 -0
- package/scripts/aicf-skills.mjs +51 -0
- package/scripts/check-release-readiness.mjs +197 -0
- package/scripts/check-skills-public.mjs +12 -0
- package/scripts/check-trigger-coverage.mjs +20 -0
- package/scripts/generate-skill-index.mjs +30 -0
- package/scripts/install-skills.mjs +31 -0
- package/scripts/lib/skill-common.mjs +511 -0
- package/scripts/validate-skills.mjs +13 -0
- package/skills/README.md +12 -0
- package/skills/aicf-action-lifecycle/SKILL.md +87 -0
- package/skills/aicf-action-lifecycle/assets/action-handler.template.ts +24 -0
- package/skills/aicf-action-lifecycle/assets/approval-record.json +9 -0
- package/skills/aicf-action-lifecycle/assets/prepared-action-record.json +11 -0
- package/skills/aicf-action-lifecycle/references/action-state-machine.md +17 -0
- package/skills/aicf-action-lifecycle/references/approval-patterns.md +9 -0
- package/skills/aicf-action-lifecycle/references/idempotency-and-audit.md +8 -0
- package/skills/aicf-action-lifecycle/references/verification-patterns.md +7 -0
- package/skills/aicf-capability-authoring/SKILL.md +100 -0
- package/skills/aicf-capability-authoring/assets/capability.manifest.yaml +31 -0
- package/skills/aicf-capability-authoring/assets/entity.manifest.yaml +12 -0
- package/skills/aicf-capability-authoring/assets/eval-case.yaml +12 -0
- package/skills/aicf-capability-authoring/references/checklist.md +13 -0
- package/skills/aicf-capability-authoring/references/examples.md +12 -0
- package/skills/aicf-capability-authoring/references/field-guide.md +13 -0
- package/skills/aicf-capability-authoring/references/manifest-decision-tree.md +10 -0
- package/skills/aicf-control-plane-ui/SKILL.md +90 -0
- package/skills/aicf-control-plane-ui/assets/control-plane-wireframe.md +29 -0
- package/skills/aicf-control-plane-ui/assets/ui-acceptance-checklist.md +11 -0
- package/skills/aicf-control-plane-ui/references/api-contracts.md +14 -0
- package/skills/aicf-control-plane-ui/references/approval-console.md +12 -0
- package/skills/aicf-control-plane-ui/references/control-plane-screens.md +14 -0
- package/skills/aicf-control-plane-ui/references/operational-ux.md +7 -0
- package/skills/aicf-controls-and-budgets/SKILL.md +88 -0
- package/skills/aicf-controls-and-budgets/assets/budget-fixture.json +15 -0
- package/skills/aicf-controls-and-budgets/assets/controls-policy.yaml +14 -0
- package/skills/aicf-controls-and-budgets/references/budget-policy.md +12 -0
- package/skills/aicf-controls-and-budgets/references/circuit-breakers.md +13 -0
- package/skills/aicf-controls-and-budgets/references/control-types.md +10 -0
- package/skills/aicf-docs-and-examples/SKILL.md +87 -0
- package/skills/aicf-docs-and-examples/assets/example-readme-template.md +21 -0
- package/skills/aicf-docs-and-examples/assets/provider-guide-template.md +17 -0
- package/skills/aicf-docs-and-examples/references/docs-style-guide.md +9 -0
- package/skills/aicf-docs-and-examples/references/example-app-checklist.md +9 -0
- package/skills/aicf-docs-and-examples/references/public-api-docs.md +11 -0
- package/skills/aicf-docs-and-examples/references/quickstart-template.md +17 -0
- package/skills/aicf-eval-authoring/SKILL.md +86 -0
- package/skills/aicf-eval-authoring/assets/eval-case.yaml +17 -0
- package/skills/aicf-eval-authoring/assets/eval-result.json +15 -0
- package/skills/aicf-eval-authoring/assets/promptfoo.config.yaml +12 -0
- package/skills/aicf-eval-authoring/references/deterministic-scorers.md +13 -0
- package/skills/aicf-eval-authoring/references/eval-rubric.md +10 -0
- package/skills/aicf-eval-authoring/references/golden-test-patterns.md +8 -0
- package/skills/aicf-eval-authoring/references/real-provider-evals.md +8 -0
- package/skills/aicf-governance-lifecycle/SKILL.md +91 -0
- package/skills/aicf-governance-lifecycle/assets/lifecycle-change-request.md +21 -0
- package/skills/aicf-governance-lifecycle/assets/riskwaiver.md +17 -0
- package/skills/aicf-governance-lifecycle/references/compatibility-checks.md +21 -0
- package/skills/aicf-governance-lifecycle/references/impact-analysis.md +13 -0
- package/skills/aicf-governance-lifecycle/references/lifecycle-state-machine.md +15 -0
- package/skills/aicf-governance-lifecycle/references/riskcompiler-rules.md +13 -0
- package/skills/aicf-migration/SKILL.md +87 -0
- package/skills/aicf-migration/assets/legacy-tool-inventory.csv +3 -0
- package/skills/aicf-migration/assets/migration-plan.md +19 -0
- package/skills/aicf-migration/references/conversion-playbook.md +9 -0
- package/skills/aicf-migration/references/legacy-tool-riskmap.md +14 -0
- package/skills/aicf-migration/references/migration-inventory.md +12 -0
- package/skills/aicf-migration/references/rollout-plan.md +12 -0
- package/skills/aicf-observability-replay/SKILL.md +88 -0
- package/skills/aicf-observability-replay/assets/replay-fixture.json +23 -0
- package/skills/aicf-observability-replay/assets/trace-to-golden-template.yaml +14 -0
- package/skills/aicf-observability-replay/references/redacted-telemetry.md +7 -0
- package/skills/aicf-observability-replay/references/replay-simulation.md +10 -0
- package/skills/aicf-observability-replay/references/trace-span-map.md +17 -0
- package/skills/aicf-observability-replay/references/trace-to-golden.md +12 -0
- package/skills/aicf-policy-and-risk/SKILL.md +88 -0
- package/skills/aicf-policy-and-risk/assets/policy-broker.template.ts +29 -0
- package/skills/aicf-policy-and-risk/assets/policy-decision-fixture.json +15 -0
- package/skills/aicf-policy-and-risk/references/deny-approval-allow-matrix.md +12 -0
- package/skills/aicf-policy-and-risk/references/policy-broker-contract.md +13 -0
- package/skills/aicf-policy-and-risk/references/risktier-rules.md +10 -0
- package/skills/aicf-policy-and-risk/references/semantic-invariants.md +9 -0
- package/skills/aicf-provider-conformance/SKILL.md +88 -0
- package/skills/aicf-provider-conformance/assets/provider-conformance-case.yaml +18 -0
- package/skills/aicf-provider-conformance/assets/provider-matrix.json +19 -0
- package/skills/aicf-provider-conformance/references/provider-normalization-matrix.md +14 -0
- package/skills/aicf-provider-conformance/references/provider-runtime-boundaries.md +10 -0
- package/skills/aicf-provider-conformance/references/schema-compatibility.md +8 -0
- package/skills/aicf-provider-conformance/references/tool-call-mapping.md +8 -0
- package/skills/aicf-release-hygiene/SKILL.md +85 -0
- package/skills/aicf-release-hygiene/assets/release-report-template.md +19 -0
- package/skills/aicf-release-hygiene/references/forbidden-artifacts.md +12 -0
- package/skills/aicf-release-hygiene/references/package-smoke-test.md +10 -0
- package/skills/aicf-release-hygiene/references/release-checklist.md +10 -0
- package/skills/aicf-release-hygiene/references/source-archive-rules.md +8 -0
- package/skills/aicf-runtime-integration/SKILL.md +87 -0
- package/skills/aicf-runtime-integration/assets/context-builder.template.ts +22 -0
- package/skills/aicf-runtime-integration/assets/handler-registry.template.ts +20 -0
- package/skills/aicf-runtime-integration/assets/runtime-integration-plan.md +23 -0
- package/skills/aicf-runtime-integration/references/capability-router-patterns.md +8 -0
- package/skills/aicf-runtime-integration/references/context-builder-patterns.md +8 -0
- package/skills/aicf-runtime-integration/references/handler-registry-patterns.md +8 -0
- package/skills/aicf-runtime-integration/references/policy-broker-adapter.md +8 -0
- package/skills/aicf-runtime-integration/references/runtime-integration-checklist.md +10 -0
- package/skills/aicf-security-redteam/SKILL.md +89 -0
- package/skills/aicf-security-redteam/assets/promptfoo-redteam.config.yaml +13 -0
- package/skills/aicf-security-redteam/assets/redteam-case.yaml +15 -0
- package/skills/aicf-security-redteam/references/prompt-injection-patterns.md +12 -0
- package/skills/aicf-security-redteam/references/redteam-acceptance.md +11 -0
- package/skills/aicf-security-redteam/references/security-test-pack-map.md +11 -0
- package/skills/aicf-security-redteam/references/tool-misuse-patterns.md +11 -0
- package/skills/aicf-skill-pack-maintenance/SKILL.md +88 -0
- package/skills/aicf-skill-pack-maintenance/assets/skill-template.md +52 -0
- package/skills/aicf-skill-pack-maintenance/references/overlap-prevention.md +9 -0
- package/skills/aicf-skill-pack-maintenance/references/plugin-packaging.md +8 -0
- package/skills/aicf-skill-pack-maintenance/references/skill-authoring-rules.md +10 -0
- package/skills/aicf-skill-pack-maintenance/references/trigger-evaluation.md +9 -0
- package/skills/aicf-storage-and-aws/SKILL.md +86 -0
- package/skills/aicf-storage-and-aws/assets/dynamodb-store.template.ts +21 -0
- package/skills/aicf-storage-and-aws/assets/postgres-store.template.ts +22 -0
- package/skills/aicf-storage-and-aws/assets/step-functions-approval.template.ts +15 -0
- package/skills/aicf-storage-and-aws/references/aws-adapter-boundaries.md +8 -0
- package/skills/aicf-storage-and-aws/references/least-privilege-iam.md +11 -0
- package/skills/aicf-storage-and-aws/references/step-functions-approval.md +10 -0
- package/skills/aicf-storage-and-aws/references/store-interface-map.md +10 -0
- package/skills/aicf-trust-redaction-retention/SKILL.md +88 -0
- package/skills/aicf-trust-redaction-retention/assets/redaction-policy.yaml +15 -0
- package/skills/aicf-trust-redaction-retention/assets/trust-labelled-context.json +18 -0
- package/skills/aicf-trust-redaction-retention/references/redaction-rules.md +8 -0
- package/skills/aicf-trust-redaction-retention/references/retention-rules.md +11 -0
- package/skills/aicf-trust-redaction-retention/references/taint-rules.md +9 -0
- package/skills/aicf-trust-redaction-retention/references/trust-labels.md +16 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
cpSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
statSync,
|
|
10
|
+
writeFileSync
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { parse as parseYaml } from "yaml";
|
|
15
|
+
|
|
16
|
+
export const requiredSections = [
|
|
17
|
+
"Purpose",
|
|
18
|
+
"Use this skill when",
|
|
19
|
+
"Do not use this skill when",
|
|
20
|
+
"Inputs to inspect first",
|
|
21
|
+
"Workflow",
|
|
22
|
+
"Required outputs",
|
|
23
|
+
"Validation",
|
|
24
|
+
"Hard rules",
|
|
25
|
+
"Handoff format"
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export const requiredMetadataKeys = [
|
|
29
|
+
"aicf.skill.version",
|
|
30
|
+
"aicf.skill.package",
|
|
31
|
+
"aicf.skill.category",
|
|
32
|
+
"aicf.skill.scope",
|
|
33
|
+
"aicf.skill.strictness"
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export const skillNamePattern = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
37
|
+
|
|
38
|
+
export const expectedSkillNames = [
|
|
39
|
+
"aicf-action-lifecycle",
|
|
40
|
+
"aicf-capability-authoring",
|
|
41
|
+
"aicf-control-plane-ui",
|
|
42
|
+
"aicf-controls-and-budgets",
|
|
43
|
+
"aicf-docs-and-examples",
|
|
44
|
+
"aicf-eval-authoring",
|
|
45
|
+
"aicf-governance-lifecycle",
|
|
46
|
+
"aicf-migration",
|
|
47
|
+
"aicf-observability-replay",
|
|
48
|
+
"aicf-policy-and-risk",
|
|
49
|
+
"aicf-provider-conformance",
|
|
50
|
+
"aicf-release-hygiene",
|
|
51
|
+
"aicf-runtime-integration",
|
|
52
|
+
"aicf-security-redteam",
|
|
53
|
+
"aicf-skill-pack-maintenance",
|
|
54
|
+
"aicf-storage-and-aws",
|
|
55
|
+
"aicf-trust-redaction-retention"
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
export const publicSafetyPatterns = [
|
|
59
|
+
{ label: "OpenAI key marker", pattern: /OPENAI_API_KEY\s*=/i },
|
|
60
|
+
{ label: "Anthropic key marker", pattern: /ANTHROPIC_API_KEY\s*=/i },
|
|
61
|
+
{ label: "Google key marker", pattern: /GOOGLE_API_KEY\s*=/i },
|
|
62
|
+
{ label: "AWS secret marker", pattern: /AWS_SECRET_ACCESS_KEY/i },
|
|
63
|
+
{ label: "private key header", pattern: /BEGIN PRIVATE KEY/i },
|
|
64
|
+
{ label: "Slack token marker", pattern: /xoxb-/i },
|
|
65
|
+
{ label: "GitHub token marker", pattern: /ghp_/i },
|
|
66
|
+
{ label: "OpenAI-style token marker", pattern: /sk-/i },
|
|
67
|
+
{ label: "AWS access key marker", pattern: /AKIA/i },
|
|
68
|
+
{ label: "private path marker", pattern: /_private\//i },
|
|
69
|
+
{ label: "raw provider payload marker", pattern: /rawProviderPayload|raw_provider_payload|raw-provider-payload/i },
|
|
70
|
+
{ label: "raw prompt marker", pattern: /rawPrompt|raw_prompt|raw-prompt/i },
|
|
71
|
+
{ label: "local Windows workspace path", pattern: /C:\\Users\\|C:\\work\\/i }
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
export const forbiddenPackageSegments = new Set([
|
|
75
|
+
".git",
|
|
76
|
+
"_private",
|
|
77
|
+
"coverage",
|
|
78
|
+
"dist",
|
|
79
|
+
"node_modules",
|
|
80
|
+
".nyc_output"
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
export const forbiddenPackageExtensions = new Set([
|
|
84
|
+
".docx",
|
|
85
|
+
".har",
|
|
86
|
+
".key",
|
|
87
|
+
".log",
|
|
88
|
+
".p12",
|
|
89
|
+
".pdf",
|
|
90
|
+
".pem",
|
|
91
|
+
".pfx",
|
|
92
|
+
".tgz",
|
|
93
|
+
".trace",
|
|
94
|
+
".zip"
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
export function packageRoot() {
|
|
98
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function normalizePath(file) {
|
|
102
|
+
return file.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function resolvePath(root, maybeRelative) {
|
|
106
|
+
if (!maybeRelative) return root;
|
|
107
|
+
return path.resolve(root, maybeRelative);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function readJson(file) {
|
|
111
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function readText(file) {
|
|
115
|
+
return readFileSync(file, "utf8");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function writeText(file, content) {
|
|
119
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
120
|
+
writeFileSync(file, content, "utf8");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function listSkillDirs(skillsRoot) {
|
|
124
|
+
if (!existsSync(skillsRoot)) return [];
|
|
125
|
+
return readdirSync(skillsRoot, { withFileTypes: true })
|
|
126
|
+
.filter((entry) => entry.isDirectory())
|
|
127
|
+
.map((entry) => path.join(skillsRoot, entry.name))
|
|
128
|
+
.sort((a, b) => normalizePath(a).localeCompare(normalizePath(b)));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function parseSkillFile(file) {
|
|
132
|
+
const content = readText(file);
|
|
133
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/u.exec(content);
|
|
134
|
+
if (!match) {
|
|
135
|
+
throw new Error("SKILL.md must start with YAML frontmatter delimited by ---.");
|
|
136
|
+
}
|
|
137
|
+
const frontmatter = parseYaml(match[1]) ?? {};
|
|
138
|
+
return {
|
|
139
|
+
content,
|
|
140
|
+
frontmatter,
|
|
141
|
+
body: match[2] ?? ""
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function validateSkills(skillsRoot, options = {}) {
|
|
146
|
+
const failures = [];
|
|
147
|
+
const skillDirs = listSkillDirs(skillsRoot);
|
|
148
|
+
|
|
149
|
+
for (const skillDir of skillDirs) {
|
|
150
|
+
failures.push(...validateSkillDirectory(skillDir, options));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return failures;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function validateSkillDirectory(skillDir, options = {}) {
|
|
157
|
+
const failures = [];
|
|
158
|
+
const skillName = path.basename(skillDir);
|
|
159
|
+
const skillFile = path.join(skillDir, "SKILL.md");
|
|
160
|
+
const relativeSkill = normalizePath(path.relative(options.root ?? path.dirname(skillDir), skillDir));
|
|
161
|
+
|
|
162
|
+
if (!existsSync(skillFile)) {
|
|
163
|
+
return [`${relativeSkill}: missing SKILL.md.`];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let parsed;
|
|
167
|
+
try {
|
|
168
|
+
parsed = parseSkillFile(skillFile);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
return [`${relativeSkill}/SKILL.md: ${error.message}`];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const { frontmatter, body, content } = parsed;
|
|
174
|
+
const name = frontmatter.name;
|
|
175
|
+
const description = frontmatter.description;
|
|
176
|
+
|
|
177
|
+
if (name !== skillName) failures.push(`${relativeSkill}/SKILL.md: frontmatter name must match directory name "${skillName}".`);
|
|
178
|
+
if (typeof name !== "string" || !skillNamePattern.test(name)) failures.push(`${relativeSkill}/SKILL.md: name must be lowercase kebab-case, 1-64 chars, with no edge hyphens.`);
|
|
179
|
+
if (typeof name === "string" && name.includes("--")) failures.push(`${relativeSkill}/SKILL.md: name must not contain consecutive hyphens.`);
|
|
180
|
+
if (typeof description !== "string" || description.trim().length === 0) failures.push(`${relativeSkill}/SKILL.md: description is required.`);
|
|
181
|
+
if (typeof description === "string" && description.length > 1024) failures.push(`${relativeSkill}/SKILL.md: description must be <=1024 characters.`);
|
|
182
|
+
if (frontmatter.license !== "MIT") failures.push(`${relativeSkill}/SKILL.md: license must be MIT.`);
|
|
183
|
+
if (typeof frontmatter.compatibility !== "string" || frontmatter.compatibility.trim().length === 0) failures.push(`${relativeSkill}/SKILL.md: compatibility is required.`);
|
|
184
|
+
if (typeof frontmatter.compatibility === "string" && frontmatter.compatibility.length > 500) failures.push(`${relativeSkill}/SKILL.md: compatibility must be <=500 characters.`);
|
|
185
|
+
|
|
186
|
+
const metadata = frontmatter.metadata;
|
|
187
|
+
if (!metadata || typeof metadata !== "object") {
|
|
188
|
+
failures.push(`${relativeSkill}/SKILL.md: metadata is required.`);
|
|
189
|
+
} else {
|
|
190
|
+
for (const key of requiredMetadataKeys) {
|
|
191
|
+
if (!(key in metadata)) failures.push(`${relativeSkill}/SKILL.md: metadata.${key} is required.`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (/^allowed-tools\s*:/im.test(content)) {
|
|
196
|
+
failures.push(`${relativeSkill}/SKILL.md: allowed-tools is not permitted in S2.`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const lineCount = content.split(/\r?\n/).length;
|
|
200
|
+
if (lineCount > 500) failures.push(`${relativeSkill}/SKILL.md: SKILL.md must be <=500 lines.`);
|
|
201
|
+
|
|
202
|
+
failures.push(...validateBodySections(relativeSkill, body));
|
|
203
|
+
failures.push(...validateReferences(skillDir, relativeSkill, body));
|
|
204
|
+
failures.push(...validatePublicSafety(skillFile, content, relativeSkill));
|
|
205
|
+
failures.push(...validateSkillSubtree(skillDir, relativeSkill));
|
|
206
|
+
|
|
207
|
+
return failures;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function validateBodySections(relativeSkill, body) {
|
|
211
|
+
const failures = [];
|
|
212
|
+
if (!/^#\s+\S/m.test(body)) {
|
|
213
|
+
failures.push(`${relativeSkill}/SKILL.md: body must include a top-level title.`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let cursor = -1;
|
|
217
|
+
for (const section of requiredSections) {
|
|
218
|
+
const pattern = new RegExp(`^##\\s+${escapeRegExp(section)}\\s*$`, "m");
|
|
219
|
+
const match = pattern.exec(body);
|
|
220
|
+
if (!match) {
|
|
221
|
+
failures.push(`${relativeSkill}/SKILL.md: missing required section "## ${section}".`);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if ((match.index ?? 0) < cursor) {
|
|
225
|
+
failures.push(`${relativeSkill}/SKILL.md: section "## ${section}" appears out of order.`);
|
|
226
|
+
}
|
|
227
|
+
cursor = match.index ?? cursor;
|
|
228
|
+
}
|
|
229
|
+
return failures;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function validateReferences(skillDir, relativeSkill, body) {
|
|
233
|
+
const failures = [];
|
|
234
|
+
const linkPattern = /!?\[[^\]]*]\(([^)]+)\)/g;
|
|
235
|
+
for (const match of body.matchAll(linkPattern)) {
|
|
236
|
+
const rawTarget = match[1]?.trim();
|
|
237
|
+
if (!rawTarget || /^(?:https?:|mailto:|#)/i.test(rawTarget)) continue;
|
|
238
|
+
const target = rawTarget.split("#")[0].split("?")[0];
|
|
239
|
+
if (!target) continue;
|
|
240
|
+
const normalized = normalizePath(target);
|
|
241
|
+
const parts = normalized.split("/");
|
|
242
|
+
if (!["references", "assets", "scripts"].includes(parts[0]) || parts.length !== 2) {
|
|
243
|
+
failures.push(`${relativeSkill}/SKILL.md: reference "${rawTarget}" must be one-level references/, assets/, or scripts/.`);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const resolved = path.resolve(skillDir, target);
|
|
247
|
+
if (!resolved.startsWith(path.resolve(skillDir)) || !existsSync(resolved)) {
|
|
248
|
+
failures.push(`${relativeSkill}/SKILL.md: reference "${rawTarget}" does not exist.`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return failures;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function validateSkillSubtree(skillDir, relativeSkill) {
|
|
255
|
+
const failures = [];
|
|
256
|
+
for (const directory of ["references", "assets"]) {
|
|
257
|
+
const fullPath = path.join(skillDir, directory);
|
|
258
|
+
if (existsSync(fullPath) && readdirSync(fullPath).length === 0) {
|
|
259
|
+
failures.push(`${relativeSkill}/${directory}: directory must not be empty.`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for (const file of walk(skillDir, { excludeSegments: new Set(["node_modules"]) })) {
|
|
264
|
+
const relative = normalizePath(path.relative(skillDir, file));
|
|
265
|
+
const content = isTextFile(file) ? readText(file) : "";
|
|
266
|
+
if (content) failures.push(...validatePublicSafety(file, content, `${relativeSkill}/${relative}`));
|
|
267
|
+
try {
|
|
268
|
+
if (file.endsWith(".json")) JSON.parse(content);
|
|
269
|
+
if (file.endsWith(".yaml") || file.endsWith(".yml")) parseYaml(content);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
failures.push(`${relativeSkill}/${relative}: failed to parse ${path.extname(file)}: ${error.message}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return failures;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function validatePublicSafety(file, content, label = normalizePath(file)) {
|
|
278
|
+
const failures = [];
|
|
279
|
+
for (const { label: patternLabel, pattern } of publicSafetyPatterns) {
|
|
280
|
+
pattern.lastIndex = 0;
|
|
281
|
+
if (pattern.test(content)) failures.push(`${label}: contains forbidden public-safety marker: ${patternLabel}.`);
|
|
282
|
+
}
|
|
283
|
+
return failures;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function loadTriggerFixture(fixturePath) {
|
|
287
|
+
const data = readJson(fixturePath);
|
|
288
|
+
if (!data || !Array.isArray(data.skills)) {
|
|
289
|
+
throw new Error(`${fixturePath}: expected { "skills": [...] }.`);
|
|
290
|
+
}
|
|
291
|
+
return data;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function checkTriggerCoverage(skillsRoot, fixturePath) {
|
|
295
|
+
const failures = [];
|
|
296
|
+
const warnings = [];
|
|
297
|
+
const skills = skillSummaries(skillsRoot);
|
|
298
|
+
const fixture = loadTriggerFixture(fixturePath);
|
|
299
|
+
const skillNames = new Set(skills.map((skill) => skill.name));
|
|
300
|
+
const fixtureNames = new Set(fixture.skills.map((entry) => entry.name));
|
|
301
|
+
|
|
302
|
+
for (const skill of skills) {
|
|
303
|
+
if (!fixtureNames.has(skill.name)) failures.push(`${skill.name}: missing trigger fixture coverage.`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
for (const entry of fixture.skills) {
|
|
307
|
+
if (!skillNames.has(entry.name)) failures.push(`${entry.name}: trigger fixture references an unknown skill.`);
|
|
308
|
+
if (!Array.isArray(entry.positive) || entry.positive.length === 0) failures.push(`${entry.name}: positive trigger examples are required.`);
|
|
309
|
+
if (!Array.isArray(entry.negative) || entry.negative.length === 0) failures.push(`${entry.name}: negative trigger examples are required.`);
|
|
310
|
+
if ((entry.positive?.length ?? 0) > 0 || (entry.negative?.length ?? 0) > 0) {
|
|
311
|
+
warnings.push(`${entry.name}: trigger prompts are static coverage examples, not model-selection tests.`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const skill = skills.find((candidate) => candidate.name === entry.name);
|
|
315
|
+
if (skill) {
|
|
316
|
+
for (const term of entry.required_description_terms ?? []) {
|
|
317
|
+
if (!skill.description.toLowerCase().includes(String(term).toLowerCase())) {
|
|
318
|
+
failures.push(`${entry.name}: description missing required term "${term}".`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return { failures, warnings };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function skillSummaries(skillsRoot) {
|
|
328
|
+
return listSkillDirs(skillsRoot).map((skillDir) => {
|
|
329
|
+
const skillFile = path.join(skillDir, "SKILL.md");
|
|
330
|
+
const parsed = parseSkillFile(skillFile);
|
|
331
|
+
const metadata = parsed.frontmatter.metadata ?? {};
|
|
332
|
+
return {
|
|
333
|
+
name: parsed.frontmatter.name,
|
|
334
|
+
directory: skillDir,
|
|
335
|
+
description: parsed.frontmatter.description,
|
|
336
|
+
category: metadata["aicf.skill.category"] ?? "",
|
|
337
|
+
references: existingFiles(path.join(skillDir, "references")),
|
|
338
|
+
primaryTriggerPhrases: triggerPhrases(parsed.frontmatter.description)
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function generateSkillIndex(skillsRoot, fixturePath) {
|
|
344
|
+
const skills = skillSummaries(skillsRoot);
|
|
345
|
+
const fixture = existsSync(fixturePath) ? loadTriggerFixture(fixturePath) : { skills: [] };
|
|
346
|
+
const fixtureByName = new Map(fixture.skills.map((entry) => [entry.name, entry]));
|
|
347
|
+
const lines = [
|
|
348
|
+
"# Skill Index",
|
|
349
|
+
"",
|
|
350
|
+
"Generated from the current AICF Agent Skills package.",
|
|
351
|
+
""
|
|
352
|
+
];
|
|
353
|
+
|
|
354
|
+
if (skills.length === 0) {
|
|
355
|
+
lines.push("No real skill folders exist yet. S3-S5 add the production AICF skills.", "");
|
|
356
|
+
return `${lines.join("\n")}\n`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
for (const skill of skills) {
|
|
360
|
+
const triggers = fixtureByName.get(skill.name) ?? {};
|
|
361
|
+
lines.push(`## ${skill.name}`);
|
|
362
|
+
lines.push("");
|
|
363
|
+
lines.push(`- Description: ${skill.description}`);
|
|
364
|
+
lines.push(`- Category: ${skill.category || "unspecified"}`);
|
|
365
|
+
lines.push(`- Reference files: ${skill.references.length > 0 ? skill.references.join(", ") : "none"}`);
|
|
366
|
+
lines.push(`- Primary trigger phrases: ${skill.primaryTriggerPhrases.join(", ") || "none"}`);
|
|
367
|
+
lines.push(`- Positive examples: ${(triggers.positive ?? []).join(" | ") || "none"}`);
|
|
368
|
+
lines.push(`- Negative examples: ${(triggers.negative ?? []).join(" | ") || "none"}`);
|
|
369
|
+
lines.push("");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return `${lines.join("\n")}\n`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function installSkills({ skillsRoot, target, force = false, allowOutside = false, root = packageRoot() }) {
|
|
376
|
+
if (!target) throw new Error("--target is required.");
|
|
377
|
+
const resolvedTarget = path.resolve(expandHome(target));
|
|
378
|
+
const repoRoot = path.resolve(root, "..");
|
|
379
|
+
const home = path.resolve(process.env.USERPROFILE || process.env.HOME || "");
|
|
380
|
+
|
|
381
|
+
if (!allowOutside && !resolvedTarget.startsWith(repoRoot) && (!home || !resolvedTarget.startsWith(home))) {
|
|
382
|
+
throw new Error(`Refusing to install outside repository or home without --allow-outside: ${resolvedTarget}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
mkdirSync(resolvedTarget, { recursive: true });
|
|
386
|
+
const installed = [];
|
|
387
|
+
for (const skillDir of listSkillDirs(skillsRoot)) {
|
|
388
|
+
const name = path.basename(skillDir);
|
|
389
|
+
const destination = path.join(resolvedTarget, name);
|
|
390
|
+
if (existsSync(destination)) {
|
|
391
|
+
if (!force) throw new Error(`${name}: target already exists. Use --force to overwrite.`);
|
|
392
|
+
rmSync(destination, { recursive: true, force: true });
|
|
393
|
+
}
|
|
394
|
+
cpSync(skillDir, destination, {
|
|
395
|
+
recursive: true,
|
|
396
|
+
filter: (source) => {
|
|
397
|
+
const normalized = normalizePath(path.relative(skillDir, source));
|
|
398
|
+
return !normalized.split("/").some((segment) => ["node_modules", "tests", ".codex-plugin"].includes(segment));
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
installed.push(name);
|
|
402
|
+
}
|
|
403
|
+
return { target: resolvedTarget, installed };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function publicPackageFailures(root = packageRoot()) {
|
|
407
|
+
const failures = [];
|
|
408
|
+
const allowlist = loadAllowlist(path.join(root, "docs", "secret-scan-allowlist.md"));
|
|
409
|
+
const packFiles = npmPackFiles(root);
|
|
410
|
+
|
|
411
|
+
for (const file of packFiles) {
|
|
412
|
+
failures.push(...forbiddenPathFailures(file, "npm package"));
|
|
413
|
+
const fullPath = path.join(root, file);
|
|
414
|
+
if (!existsSync(fullPath) || !isTextFile(fullPath)) continue;
|
|
415
|
+
const content = readText(fullPath);
|
|
416
|
+
if (file !== "scripts/lib/skill-common.mjs") {
|
|
417
|
+
for (const { label, pattern } of publicSafetyPatterns) {
|
|
418
|
+
pattern.lastIndex = 0;
|
|
419
|
+
if (pattern.test(content) && !allowlist.has(label)) {
|
|
420
|
+
failures.push(`${file}: contains forbidden public-safety marker: ${label}.`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
if (file.endsWith(".json")) JSON.parse(content);
|
|
426
|
+
if (file.endsWith(".yaml") || file.endsWith(".yml")) parseYaml(content);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
failures.push(`${file}: parse failure: ${error.message}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return failures;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function npmPackFiles(root = packageRoot()) {
|
|
436
|
+
const npmExecPath = process.env.npm_execpath;
|
|
437
|
+
const command = npmExecPath ? process.execPath : process.platform === "win32" ? "cmd.exe" : "npm";
|
|
438
|
+
const args = npmExecPath
|
|
439
|
+
? [npmExecPath, "pack", "--dry-run", "--json"]
|
|
440
|
+
: process.platform === "win32"
|
|
441
|
+
? ["/d", "/s", "/c", "npm pack --dry-run --json"]
|
|
442
|
+
: ["pack", "--dry-run", "--json"];
|
|
443
|
+
const output = execFileSync(command, args, {
|
|
444
|
+
cwd: root,
|
|
445
|
+
encoding: "utf8",
|
|
446
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
447
|
+
});
|
|
448
|
+
return JSON.parse(output)[0].files.map((file) => normalizePath(file.path));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function forbiddenPathFailures(file, context = "source") {
|
|
452
|
+
const failures = [];
|
|
453
|
+
const normalized = normalizePath(file);
|
|
454
|
+
const segments = normalized.split("/");
|
|
455
|
+
if (segments.some((segment) => forbiddenPackageSegments.has(segment))) failures.push(`${context}: forbidden path included: ${normalized}`);
|
|
456
|
+
if (segments.some((segment) => segment === ".DS_Store" || segment === "Thumbs.db")) failures.push(`${context}: forbidden platform artifact included: ${normalized}`);
|
|
457
|
+
if (segments.some((segment) => segment === ".env" || segment.startsWith(".env."))) failures.push(`${context}: forbidden environment file included: ${normalized}`);
|
|
458
|
+
const extension = path.extname(normalized).toLowerCase();
|
|
459
|
+
if (forbiddenPackageExtensions.has(extension)) failures.push(`${context}: forbidden artifact type included: ${normalized}`);
|
|
460
|
+
return failures;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function walk(directory, options = {}) {
|
|
464
|
+
if (!existsSync(directory)) return [];
|
|
465
|
+
const excludeSegments = options.excludeSegments ?? new Set();
|
|
466
|
+
const files = [];
|
|
467
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
468
|
+
if (excludeSegments.has(entry.name)) continue;
|
|
469
|
+
const fullPath = path.join(directory, entry.name);
|
|
470
|
+
if (entry.isDirectory()) files.push(...walk(fullPath, options));
|
|
471
|
+
if (entry.isFile()) files.push(fullPath);
|
|
472
|
+
}
|
|
473
|
+
return files.sort((a, b) => normalizePath(a).localeCompare(normalizePath(b)));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function isTextFile(file) {
|
|
477
|
+
return /\.(cjs|css|html|js|json|md|mjs|svg|ts|txt|yaml|yml)$/i.test(file) || !path.extname(file);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function existingFiles(directory) {
|
|
481
|
+
if (!existsSync(directory)) return [];
|
|
482
|
+
return readdirSync(directory, { withFileTypes: true })
|
|
483
|
+
.filter((entry) => entry.isFile())
|
|
484
|
+
.map((entry) => `references/${entry.name}`)
|
|
485
|
+
.sort();
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function triggerPhrases(description = "") {
|
|
489
|
+
const lower = String(description).replace(/\s+/g, " ").trim();
|
|
490
|
+
const useWhen = lower.match(/\bUse when\b(.+)$/i)?.[1]?.trim();
|
|
491
|
+
if (!useWhen) return [];
|
|
492
|
+
return useWhen.split(/,|\bor\b|\band\b/i).map((part) => part.trim()).filter(Boolean).slice(0, 5);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function escapeRegExp(value) {
|
|
496
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function expandHome(value) {
|
|
500
|
+
if (value === "~") return process.env.USERPROFILE || process.env.HOME || value;
|
|
501
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
502
|
+
const home = process.env.USERPROFILE || process.env.HOME;
|
|
503
|
+
if (home) return path.join(home, value.slice(2));
|
|
504
|
+
}
|
|
505
|
+
return value;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function loadAllowlist(file) {
|
|
509
|
+
if (!existsSync(file)) return new Set();
|
|
510
|
+
return new Set(readText(file).split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#")));
|
|
511
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { packageRoot, resolvePath, validateSkills } from "./lib/skill-common.mjs";
|
|
3
|
+
|
|
4
|
+
const root = packageRoot();
|
|
5
|
+
const skillsRoot = resolvePath(root, process.argv[2] ?? "./skills");
|
|
6
|
+
const failures = validateSkills(skillsRoot, { root });
|
|
7
|
+
|
|
8
|
+
if (failures.length > 0) {
|
|
9
|
+
console.error(failures.join("\n"));
|
|
10
|
+
process.exitCode = 1;
|
|
11
|
+
} else {
|
|
12
|
+
console.log(`Skill validation passed for ${skillsRoot}.`);
|
|
13
|
+
}
|
package/skills/README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Skills
|
|
2
|
+
|
|
3
|
+
This directory is the source of truth for AICF Agent Skills.
|
|
4
|
+
|
|
5
|
+
It contains the complete 17-skill v1 inventory for AICF builder workflows: capability
|
|
6
|
+
authoring, governance, policy/risk, evals, security, runtime, action lifecycle,
|
|
7
|
+
trust/redaction/retention, observability/replay, controls, provider conformance,
|
|
8
|
+
optional storage/AWS guidance, migration, docs/examples, control-plane UI/API guidance,
|
|
9
|
+
release hygiene, and skill-pack maintenance.
|
|
10
|
+
|
|
11
|
+
Do not mirror these folders into local discovery directories by default. Use the package
|
|
12
|
+
install script when a local copy is needed.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: aicf-action-lifecycle
|
|
3
|
+
description:
|
|
4
|
+
Implement or audit AICF propose, prepare, preview, approve, commit, verify, audit,
|
|
5
|
+
idempotency, and rollback-safe action workflows for side-effecting AI capabilities.
|
|
6
|
+
license: MIT
|
|
7
|
+
compatibility:
|
|
8
|
+
Codex and Agent Skills-compatible coding agents. Works in repositories using AICF or
|
|
9
|
+
migrating to AICF.
|
|
10
|
+
metadata:
|
|
11
|
+
aicf.skill.version: "1.0.0-rc.4"
|
|
12
|
+
aicf.skill.package: "@aicf/agent-skills"
|
|
13
|
+
aicf.skill.category: "action-lifecycle"
|
|
14
|
+
aicf.skill.scope: "builder"
|
|
15
|
+
aicf.skill.strictness: "fail-closed-runtime"
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# AICF Action Lifecycle
|
|
19
|
+
|
|
20
|
+
## Purpose
|
|
21
|
+
|
|
22
|
+
Guide implementation or review of side-effect workflows so prepare, approval, commit,
|
|
23
|
+
verify, idempotency, and audit paths stay host-controlled and fail closed.
|
|
24
|
+
|
|
25
|
+
## Use this skill when
|
|
26
|
+
|
|
27
|
+
- Adding or auditing prepare, approval, commit, verify, audit, or idempotency behavior.
|
|
28
|
+
- Linking prepare capabilities to host-controlled commit capabilities.
|
|
29
|
+
- Testing state transitions for side-effecting capabilities.
|
|
30
|
+
|
|
31
|
+
## Do not use this skill when
|
|
32
|
+
|
|
33
|
+
- Creating read-only capability manifests.
|
|
34
|
+
- Exposing commit tools to models.
|
|
35
|
+
- Building production approval UI or durable storage unless explicitly requested.
|
|
36
|
+
|
|
37
|
+
## Inputs to inspect first
|
|
38
|
+
|
|
39
|
+
- Capability lifecycle metadata, linked prepare/commit IDs, runtime action manager,
|
|
40
|
+
stores, audit sink, handlers, and tests.
|
|
41
|
+
- References: [state machine](references/action-state-machine.md),
|
|
42
|
+
[approval patterns](references/approval-patterns.md),
|
|
43
|
+
[idempotency and audit](references/idempotency-and-audit.md), and
|
|
44
|
+
[verification patterns](references/verification-patterns.md).
|
|
45
|
+
|
|
46
|
+
## Workflow
|
|
47
|
+
|
|
48
|
+
1. Identify side effects: create, update, delete, send, money movement, permission
|
|
49
|
+
changes, external workflows, or irreversible effects.
|
|
50
|
+
2. Keep the model-exposed operation read or prepare by default.
|
|
51
|
+
3. Link prepare to the allowed commit capability explicitly.
|
|
52
|
+
4. Wire prepared action, approval, idempotency, audit, and verification paths.
|
|
53
|
+
5. Enforce valid transitions: proposed, prepared, approval required, approved, rejected,
|
|
54
|
+
committed, failed, expired, cancelled.
|
|
55
|
+
6. Prevent duplicate commits with scoped idempotency.
|
|
56
|
+
7. Mark failed commit attempts as failed and preserve audit evidence.
|
|
57
|
+
8. Add tests for every valid and invalid transition.
|
|
58
|
+
|
|
59
|
+
Use [action handler template](assets/action-handler.template.ts),
|
|
60
|
+
[prepared action record](assets/prepared-action-record.json), and
|
|
61
|
+
[approval record](assets/approval-record.json) as safe examples.
|
|
62
|
+
|
|
63
|
+
## Required outputs
|
|
64
|
+
|
|
65
|
+
- Lifecycle implementation or audit notes with linked prepare/commit behavior.
|
|
66
|
+
- Tests for success, approval, rejection, expiry, duplicate commit, invalid transition,
|
|
67
|
+
and handler failure.
|
|
68
|
+
- Safe audit/idempotency summary.
|
|
69
|
+
|
|
70
|
+
## Validation
|
|
71
|
+
|
|
72
|
+
- Run runtime lifecycle tests and package checks used by the repository.
|
|
73
|
+
- Confirm commit uses stored prepared action args.
|
|
74
|
+
- Confirm terminal states cannot return to pending or approved.
|
|
75
|
+
|
|
76
|
+
## Hard rules
|
|
77
|
+
|
|
78
|
+
- Commit must not be executable through model-originated tool calls.
|
|
79
|
+
- Commit must require the correct stored prepared action and approval when policy
|
|
80
|
+
requires it.
|
|
81
|
+
- Invalid output, thrown handler, or failed commit must move the action to failed.
|
|
82
|
+
- Do not store private raw data in audit records.
|
|
83
|
+
|
|
84
|
+
## Handoff format
|
|
85
|
+
|
|
86
|
+
Report lifecycle states changed, handlers wired, idempotency scope, audit evidence,
|
|
87
|
+
tests run, and remaining approval/storage obligations.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type ExamplePreparedAction = {
|
|
2
|
+
preparedActionId: string;
|
|
3
|
+
capabilityId: string;
|
|
4
|
+
commitCapabilityId: string;
|
|
5
|
+
argsHash: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export async function prepareExampleAction(
|
|
9
|
+
args: Record<string, unknown>
|
|
10
|
+
): Promise<ExamplePreparedAction> {
|
|
11
|
+
return {
|
|
12
|
+
preparedActionId: "prepared_example_001",
|
|
13
|
+
capabilityId: "example.action.prepare",
|
|
14
|
+
commitCapabilityId: "example.action.commit",
|
|
15
|
+
argsHash: `hash_${Object.keys(args).sort().join("_")}`
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function commitExampleAction(prepared: ExamplePreparedAction) {
|
|
20
|
+
return {
|
|
21
|
+
committedActionId: `commit_${prepared.preparedActionId}`,
|
|
22
|
+
status: "committed"
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"approvalId": "approval_example_001",
|
|
3
|
+
"preparedActionId": "prepared_example_001",
|
|
4
|
+
"state": "pending",
|
|
5
|
+
"requesterRef": "subject_hash_requester",
|
|
6
|
+
"deciderRef": null,
|
|
7
|
+
"reason": "Synthetic approval required for example side effect.",
|
|
8
|
+
"createdAt": "2026-06-08T00:00:00.000Z"
|
|
9
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"preparedActionId": "prepared_example_001",
|
|
3
|
+
"capabilityId": "example.action.prepare",
|
|
4
|
+
"commitCapabilityId": "example.action.commit",
|
|
5
|
+
"state": "approval_required",
|
|
6
|
+
"tenantRef": "tenant_hash_example",
|
|
7
|
+
"accountRef": "account_hash_example",
|
|
8
|
+
"argsHash": "hash_example",
|
|
9
|
+
"createdAt": "2026-06-08T00:00:00.000Z",
|
|
10
|
+
"expiresAt": "2026-06-09T00:00:00.000Z"
|
|
11
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Action State Machine
|
|
2
|
+
|
|
3
|
+
Typical states:
|
|
4
|
+
|
|
5
|
+
- proposed
|
|
6
|
+
- prepared
|
|
7
|
+
- approval required
|
|
8
|
+
- approved
|
|
9
|
+
- rejected
|
|
10
|
+
- committing
|
|
11
|
+
- committed
|
|
12
|
+
- failed
|
|
13
|
+
- expired
|
|
14
|
+
- cancelled
|
|
15
|
+
|
|
16
|
+
Terminal states such as committed, failed, expired, cancelled, and rejected must not
|
|
17
|
+
move back to approved or pending.
|