Capability Extensions
Extend what an agent can do through capability packages, external evidence, persistent state, and standard protocols.
How do we add Skills, retrieval, memory, and external protocol capabilities to an agent?Concept calibration
Skill
- What it is
- A reusable capability package containing instructions, resources, operating boundaries, versioning, and compatibility metadata.
- What it is not
- It is not merely a prompt, and its scripts must not be assumed safe to execute automatically.
Tool
- What it is
- A constrained capability an agent can invoke through explicit arguments, permissions, and result contracts.
- What it is not
- A tool is not the same as MCP; a local function, command, or HTTP client can also be a tool.
Skills, Prompts & Agent Packages
Package the requirement extractor as a Skill with a manifest, resource boundaries, and compatibility metadata.
Python 3.12 · pytest · Pydantic · SQLite · deterministic MockConcept calibration\n\nPut the concepts used in this module back inside their engineering boundaries before you work with the code.\n\n\n\n### Skill skill\n\n- What it is: A reusable capability package containing instructions, resources, operating boundaries, versioning, and compatibility metadata that an agent platform can discover, install, or reference.\n- What it is not: It is not merely a prompt, and bundled scripts are not automatically trusted; execution remains constrained by platform, tool contracts, and permission policy.\n- Relationship to adjacent concepts: A prompt may be part of a Skill, a Skill may reference tools, the harness controls installation and runtime boundaries, and adapters handle platform differences.\n- Where it lives in HeatStack Forge: Forge packages requirement extraction as a Skill with a manifest, tests, resource inventory, and risk notes, then adapts it to three target platforms.\n- Typical misuse and correction: A common misuse copies one prompt file and claims cross-platform compatibility. Declare dependencies, entry points, resources, versions, permissions, and acceptance tests.\n\n### Tool tool\n\n- What it is: A constrained capability an agent invokes through explicit arguments, permissions, execution boundaries, and result contracts, such as a function, command, database query, or HTTP client.\n- What it is not: A tool is not MCP or any arbitrary script. A protocol can expose capability, while the harness still decides whether invocation is allowed and how side effects are handled.\n- Relationship to adjacent concepts: The agent loop selects tools, schemas constrain arguments, the harness enforces permission and isolation, and verification checks whether results advanced the goal.\n- Where it lives in HeatStack Forge: Forge stores tool contracts and risk levels, produces a change plan before execution, waits for approval on high-risk writes, and records results and recovery data.\n- Typical misuse and correction: A common misuse lets the model invent arguments from a tool name. Use strict schemas, deterministic validation, timeouts, idempotency keys, and refusal tests.\n\n
Validate that a capability package has entry instructions, resource directories, and reviewable relative paths
Start with a concrete problem
Create forge/skill_layout.py and make the structure validation tests pass
The file forge/skill_layout.py does not exist yet, so tests/test_stage.py raises ModuleNotFoundError when it tries to import validate_layout, and the learner must create the module from scratch.
The validate_layout function to implement receives a list of path strings and returns a list of error strings; it must reject a directory that lacks SKILL.md and reject absolute paths or parent traversal paths containing ..
The tests test_skill_requires_entry_instructions and test_parent_traversal_is_rejected lock these two structural constraints, and the learner must move both tests from failing to passing.
Make a prediction
If validate_layout([“references/guide.md”]) is implemented as just return [], what happens when test_skill_requires_entry_instructions runs?
- The test passes because references/guide.md is a valid path.
- The test fails because the assertion expects the error list to contain missing SKILL.md.
- The test fails because the path contains parent directory traversal.
- The test passes because references is an allowed root directory.
Reasoning guide: The correct option is the second one: the test asserts that “missing SKILL.md” is in validate_layout(…), and an empty list does not contain that error, so the test fails and exposes that the entry file contract is not enforced.
What it is
A Skill layout validator is a pure function contract checker
validate_layout receives a list of path strings and returns a list of error strings; it does not read file contents, but judges whether the path set satisfies the SKILL.md entry, allowed roots, and relative path safety.
The function uses PurePosixPath to normalize each path string into an inspectable path object, then performs static checks through set membership and the parts attribute.
What it is not
It is not a filesystem scanner or an installer
validate_layout does not check whether files actually exist, create directories, copy resources, or execute scripts; it only performs structural contract validation on the supplied path strings.
It also does not handle platform permission policy or tool invocation isolation, because those boundaries belong to later Harness and adapter stages, while this stage only validates Skill package structure.
How it relates to neighboring concepts
Relationships among the entry set, allowed root set, and path objects
The REQUIRED set defines the entry filename SKILL.md that must be present, while ALLOWED_ROOTS defines allowed first-level directories such as references, templates, scripts, and tests.
The parts attribute of PurePosixPath splits a path into components so the code can check the first-level root and the .. component; the validation order first checks the missing entry, then path safety, then the root directory, forming a layered check.
Boundary decision for this stage: When the path list lacks SKILL.md, return missing SKILL.md; when a path is absolute or contains .., return unsafe path; when the first-level directory is not in ALLOWED_ROOTS, return unknown root.
Distinguish readable resources from executable scripts and deny script installation or execution by default
Start with a concrete problem
Why Scripts in a Skill Package Cannot Be Auto-Installed Like Resources
During the installation of a Skill package, the core decision the learner faces is distinguishing readable static resources from executable scripts that may produce side effects. The current forge/package_policy.py file does not exist yet, so the test collection phase is interrupted by a ModuleNotFoundError.
In the failure fixture, the decide_file function treats all paths identically by marking them as installable, and it marks files under the scripts/ directory as executable. This causes scripts/setup.ps1 to be accepted and granted execution permissions without explicit user approval, violating the security policy.
The learner must create forge/package_policy.py and implement the decide_file function so that scripts are denied installation by default, and even when approved they must be stored in a non-executable state, while ordinary resource files can be safely installed.
Make a prediction
When decide_file("scripts/setup.ps1") is called without passing allow_scripts=True, what decision result should the function return?
- install=True, executable=True, because scripts are part of the package
- install=False, executable=False, because scripts require explicit approval
- install=True, executable=False, because scripts can be installed but not executed
- install=False, executable=True, because scripts need approval but can be executed
Reasoning guide: The correct answer is install=False and executable=False. Scripts carry the risk of execution side effects, so the default policy must deny their installation and state in the reason that explicit approval is required. Even if approval is later granted, the script must be stored in a non-executable state, with execution permissions controlled separately by the Harness and permission policy.
What it is
The Essential Difference Between Resources and Scripts
Resources are readable static content, such as Markdown documents, JSON configurations, or images, that do not produce system side effects when installed and read. Scripts are files containing executable code, such as .ps1 or .py, which can modify the file system, initiate network requests, or access sensitive data once executed.
The responsibility of the decide_file function is to output a FileDecision containing install, executable, and reason for each file based on its path prefix and call parameters. This decision model shifts the security policy from runtime to installation time, ensuring the platform does not inadvertently introduce executable side effects.
What it is not
Common Misconceptions About the Policy Boundary
The policy is not merely checking file extensions, because a .py file could be either an executable script or a read-only document fragment, so this stage uses the directory prefix scripts/ as the script identifier.
Approving a script for installation does not mean granting execution permissions, as allow_scripts=True only allows the script file to be copied to the installation directory, but the executable field must always be False, with actual execution decided by the Harness in a separate permission check.
How it relates to neighboring concepts
Dependencies Between Decisions and Platform Execution
The FileDecision returned by decide_file is the basis for the installer to decide whether to write the file to disk, while the executable field instructs the installer to strip or preserve the executable bit of the file during writing.
The Harness is responsible for deciding whether to actually run a script based on platform policy and user permissions after installation is complete, so the package_policy module only needs to focus on the static properties of the installation boundary and should not attempt to predict the runtime context.
Boundary decision for this stage: When the path starts with scripts/ and allow_scripts is False, the function must return install=False; when the path contains an absolute path or .., it must be rejected as unsafe; for all files that pass the checks, the executable field must be False.
Render prompts from declared variables, rejecting missing or undeclared values to avoid platform-specific hidden dependencies
Start with a concrete problem
Missing variable silently replaced by an empty string
In the current starting state, the file forge/prompt_template.py does not exist, so the test file tests/test_stage.py raises a ModuleNotFoundError when it tries to import from forge.prompt_template import render, causing collection to fail immediately.
This stage requires creating forge/prompt_template.py and implementing the render function so that it extracts variables from the template, validates the declared set and the supplied values dictionary, and finally performs safe string substitution.
The fault tree’s faultySource shows a defective initial implementation that uses values.get(match.group(1), "") during substitution; when the template declares a goal variable but the values dictionary does not provide that key, goal is silently replaced by an empty string, causing the rendered instruction to lose critical information.
The test_missing_declared_value_is_not_silently_blank test case expects a ValueError containing missing variables to be raised in this situation, rather than silently returning a truncated string.
Make a prediction
When the template is Review {{workspace}} for {{goal}}, the declared set is {workspace, goal}, but the values dictionary only provides {workspace: 'repo'}, what does an implementation using values.get(name, "") produce?
- Raises ValueError because goal is missing
- Returns ’Review repo for ’ with goal becoming an empty string
- Returns ‘Review repo for {{goal}}’ keeping the placeholder intact
- Raises KeyError because the dictionary lacks the goal key
Reasoning guide: values.get(name, "") returns the default empty string when the key is absent, so it does not raise an exception; instead it replaces goal with nothing and returns ’Review repo for ’. This is exactly the silent loss behavior that must be avoided.
What it is
Three-set validation model for declarative template rendering
The render function operates around three sets: the set of variables actually used in the template (used), the set of legally declared variables (declared), and the set of keys in the values dictionary supplied by the caller.
Before rendering, three checks must run in sequence: whether used is a subset of declared to reject undeclared variables, whether used is fully covered by values to reject missing values, and whether values contains extra keys outside declared to reject unknown values.
Only after all three validation layers pass does the function execute VARIABLE.sub for substitution, and during substitution it uses direct indexing values[match.group(1)] instead of .get, ensuring there is no fallback to a default value.
What it is not
Template rendering is not simple string substitution
Template rendering is not equivalent to unconditional str.replace or dict.get with a default, because both approaches silently produce incomplete results instead of raising an error when a variable is missing.
The declared set is not an optional decorative parameter; it is the contract for cross-platform portability: if a template uses a variable not declared in declared, the template depends on implicit context that the target platform may not support.
The extra check is not redundant strictness; it prevents callers from mistakenly believing certain values will be used by the template when they never appear in it, a mismatch that usually indicates the caller and template are out of version sync.
How it relates to neighboring concepts
Relationship between render and Skill portability
The render function is the infrastructure for cross-platform rendering of Prompt templates within a Skill package: when different platforms load the same Skill, the rendering result is consistent as long as the values dictionary satisfies the declared contract.
The decide_file function in forge/package_policy.py governs file-level policy, while render guarantees Prompt variable completeness at the content level; together they form the reliability boundary of a Skill package.
The VARIABLE regex r"{{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*}}" defines the template syntax, allowing whitespace around variable names like {{ workspace }}, which aligns with the spaced template tested in test_declared_template_renders_portably.
Boundary decision for this stage: When used - declared is non-empty, a ValueError must be raised rather than ignoring undeclared variables, because an undeclared variable means the template depends on implicit context outside the platform contract and silently accepting it would break cross-platform portability.
Parse a strict manifest containing ID, version, entrypoint, file hashes, and permission declarations
Start with a concrete problem
The manifest is a trust boundary, not a dictionary
In Stage 04, forge/manifest.py does not yet exist, so the test file tests/test_stage.py raises ModuleNotFoundError: No module named 'forge.manifest' during collection, causing the entire test run to fail before any test executes.
The starting code for this stage is null, meaning the learner must create forge/manifest.py from scratch and implement Manifest.parse so that it raises a ValueError containing the substring entrypoint when the entrypoint file is not present in the files hash map.
The core decision is that the manifest must never be parsed as a loose metadata dictionary; it must act as the Skill package trust boundary, rejecting any unverified entrypoint or file hash before construction.
Make a prediction
When Manifest.parse receives a payload whose entrypoint is missing.md but whose files map only contains SKILL.md, what should a strict parser do?
- Accept and return a Manifest object because all top-level fields are present
- Raise a ValueError whose message contains the substring entrypoint
- Automatically rewrite the entrypoint to the first key in files
- Ignore the entrypoint field and default to SKILL.md
Reasoning guide: The correct answer is to raise a ValueError whose message contains the substring entrypoint. The trust boundary requires the entrypoint to exist in the file hash map, otherwise the entrypoint is not content-addressed and cannot be trusted.
What it is
The manifest is a content-addressed trust contract
A Manifest is an immutable data contract that transforms an external dictionary into a trusted Manifest object through the Manifest.parse class method, where the parsing process itself is a complete validation pass.
It requires the field set to exactly match id, version, entrypoint, files, and permissions with no extra or missing keys, the ID must match a lowercase hyphenated pattern, and every file hash must be a 64-character hexadecimal string.
The entrypoint file must exist as a key in the files map, which means the entrypoint itself is content-addressed and an attacker cannot inject an entrypoint file that has not been hash-verified.
What it is not
The manifest is not a loose metadata container
The manifest is not a simple dictionary wrapper and must not use payload.get with default values to skip missing fields, because that would let an attacker omit critical fields and have the parser silently fill them in.
It is also not a general-purpose configuration parser, so it does not tolerate extra fields or malformed hash values; any deviation must raise a ValueError rather than silently correcting the input.
Manifest validation does not depend on a later step in the toolchain, because the parse itself must be the atomic trust decision point that accepts or rejects the package.
How it relates to neighboring concepts
Causal chain from field-set validation to entrypoint existence
The exact field-set check is the first line of defense, ensuring there are no extra or missing keys, which prevents an attacker from injecting unknown fields to bypass downstream validation logic.
Hash format validation and entrypoint existence form a causal chain: only when every value in files is a valid 64-character hex hash does the entrypoint existence check become meaningful, because content addressing depends on hash integrity.
Requiring entrypoint to be a key in files creates a strong binding between the entrypoint file and its declared content hash, so the harness can verify at install time that the entrypoint file content matches the declared hash.
Boundary decision for this stage: When the set of keys in payload is not exactly equal to {'id','version','entrypoint','files','permissions'}, the parser must immediately raise a ValueError and must never use default values to fill missing fields or silently ignore extra fields.
Parse SemVer and determine whether an upgrade requires migration from the major version
Start with a concrete problem
Major version change is not recognized as migration-required during a Skill upgrade
In the current faulty implementation of forge/versioning.py, the upgrade_kind function unconditionally returns compatible-upgrade, causing an upgrade from 1.4.2 to 2.0.0 to fail to trigger a migration requirement.
The test case test_major_upgrade_requires_migration expects the function to return migration-required when the major version changes, but the actual return value is compatible-upgrade, causing the assertion to fail.
The root cause is that version metadata is parsed and stored in the Version dataclass but is never connected to compatibility semantics, so the major version change drives no classification logic.
Make a prediction
When upgrade_kind("1.4.2", "2.0.0") is called, the faulty code returns compatible-upgrade. To fix this, which version component should the core logic compare?
- Compare only whether the patch number changed
- Compare whether the major version numbers differ
- Compare whether the minor version number increased
- Compare the lexicographic order of the full version strings
Reasoning guide: The correct approach is to compare the major version numbers. According to the SemVer specification, a major version change indicates an incompatible API change and must return migration-required, while minor and patch changes are compatible upgrades.
What it is
Semantic Versioning (SemVer) compatibility classification model
A semantic version consists of three numeric components: major, minor, and patch. A major version change indicates an incompatible API modification, a minor version change indicates a backward-compatible new feature, and a patch change indicates a backward-compatible bug fix.
The upgrade_kind function uses parse_version to parse the version strings into Version dataclass objects, then compares the major version numbers of the left and right sides to classify the upgrade type, returning migration-required if they differ and compatible-upgrade otherwise.
What it is not
Boundaries of the SemVer classification model
Version compatibility classification is not a simple string or lexicographic comparison, because 2.0.0 is lexicographically greater than 1.9.9 but this does not directly express API compatibility semantics.
The upgrade_kind function is not a general-purpose parser for arbitrary version formats; it strictly relies on the regular expression PATTERN to match standard SemVer format and cannot handle strings with non-numeric prefixes or missing components.
How it relates to neighboring concepts
Causal chain from version parsing to upgrade classification
The parse_version function uses the regular expression PATTERN to extract numeric components and construct a Version object, which uses the @dataclass(frozen=True, order=True) decorator to support natural tuple-based ordering comparisons.
The upgrade_kind function depends on the output of parse_version, first intercepting non-incrementing target versions via right <= left, then mapping a major version difference to migration-required through right.major != left.major, forming a complete causal chain from string to classification result.
Boundary decision for this stage: When the target version’s major version number differs from the current version’s, regardless of how the minor or patch numbers change, the upgrade must be classified as migration-required because a major version change is the sole indicator of a breaking API change in SemVer.
Produce compatibility or gap reports from platform capabilities, required features, and minimum client versions
Start with a concrete problem
A Skill is always misjudged as compatible on the target platform
Before the current code change, the project does not contain a forge/compatibility.py file, so tests fail during collection with ModuleNotFoundError: No module named 'forge.compatibility'.
To pass the tests, we need to create that file and implement the compatibility function. However, if we simply make the function unconditionally return True, [], a more subtle engineering defect arises: a platform lacking required features will still be incorrectly marked as compatible.
The test_missing_platform_feature_is_reported test case explicitly requires that when the platform Platform("codex", "1.2.0", frozenset({"prompts"})) lacks the resources feature, the function must return ok=False and gaps must contain "missing feature: resources".
The core task of this stage is to establish a mechanism that produces precise gap reports by comparing the platform’s actual feature set against the required feature set, and by comparing the client version against the minimum version requirement.
Make a prediction
When the compatibility function receives a platform with a sufficient client version but an incomplete feature set, and the function body performs absolutely no set comparison logic, what happens to the assertion ok is False and gaps == ["missing feature: resources"]?
- The assertion succeeds because meeting the version requirement implies feature compatibility
- The assertion fails because the actual value of
okisTrueinstead ofFalse - A
KeyErroris raised becauseresourcesis not in the platform feature set - A
TypeErroris raised because subtraction betweenfrozensetandsetis impossible
Reasoning guide: The correct answer is that the assertion fails. If the function unconditionally returns True, [], then the value of ok is True, while the assertion expects it to be False, so assert (True is False) triggers an AssertionError. This demonstrates that compatibility cannot be judged by mere existence; a substantive set difference comparison must be performed.
What it is
Compatibility Assessment Model
Compatibility assessment is a deterministic computation process based on set differences and version comparisons. It takes the current state of the target platform (including its feature set and client version number) and the hard requirements of the Skill (required features and minimum version), outputting a boolean and a list of gaps.
In this model, the difference set required_features - platform.features directly exposes the features missing from the platform, while the parse_version function converts version strings into comparable tuples to determine whether the client version is below the minimum requirement.
What it is not
Boundaries of Compatibility Assessment
Compatibility assessment is not simply checking whether a platform name exists, nor is it a heuristic-based fuzzy guess. It is not responsible for automatically installing missing features or upgrading the client version.
This mechanism does not involve evaluating the runtime performance of the platform, nor does it check whether scripts bundled with a Skill are trusted by the platform to execute; those concerns belong to the Harness and permission policy domains, which are outside the scope of this stage’s pure data comparison.
How it relates to neighboring concepts
Component Dependencies
The compatibility function depends on the parse_version function from the forge.versioning module to parse version strings like “1.2.0” into a comparable numeric structure.
The Platform dataclass serves as the input data carrier, and its features field must be of type frozenset to allow efficient set operations, while required_features is passed in as a set; subtracting the two yields an iterator over missing features.
The generation order of the gap list is guaranteed by the sorted() function, which allows test assertions to match the list contents precisely, avoiding test instability caused by the unordered nature of sets.
Boundary decision for this stage: When the platform feature set is a proper subset of required_features, it must be immediately judged as incompatible and the specific missing feature items must be reported, even if the client version meets the requirement this conclusion cannot be changed.
Build a stable, hash-verifiable release inventory for all three scenarios without auto-executing scripts
Start with a concrete problem
Release inventory lacks scenario completeness validation, allowing partial scenario packages
At the current starting point, the file forge/packaging.py does not exist, so test collection fails immediately with ModuleNotFoundError: No module named 'forge.packaging'. As the developer, you must create this module from scratch and implement the build_inventory function to generate a deterministic release inventory.
In the fault lab’s defective code, the build_inventory function completely ignores the scenarios parameter and iterates over the files dictionary directly to produce the inventory. This means that when a caller passes only {'software', 'office'}, the system still generates a release inventory, thereby accepting an incomplete scenario package.
According to this module’s milestone requirement, the release inventory must enforce coverage of all three scenarios: software, office, and music. If the scenario set does not match the requirement, the function must raise a ValueError containing a specific error message to block the release of an incomplete package.
Make a prediction
When build_inventory receives {'software', 'office'} as the scenario parameter, what should the function do to guarantee release inventory completeness?
- Return an empty list to skip the incomplete scenarios
- Raise a ValueError containing ‘software, office, and music’
- Automatically add the missing music scenario and continue generating the inventory
- Print a warning message and return the inventory normally
Reasoning guide: The correct behavior is to immediately halt execution and raise a ValueError, because the release inventory contract requires exactly three scenarios and any omission means the package is incomplete and must not pass validation.
What it is
The construction contract for a deterministic release inventory
A deterministic release inventory is an ordered record of every file in a Skill package, where each entry contains the file path, a SHA-256 hash computed from the file content, and an executable flag. It uses fixed sorting rules and hash verification to ensure that the same input file set always produces the exact same inventory output at any time and in any environment.
In this stage, determinism also extends to the scenario completeness constraint: build_inventory must require the scenarios parameter to equal exactly {'software', 'office', 'music'} and validate this strictly before generating the inventory, thereby intercepting incomplete scenario configurations at the packaging stage.
What it is not
The release inventory is not a simple file list copy
The release inventory is not a direct iteration over the input files dictionary, because dictionary insertion order may vary across runs, which would break determinism. It is also not a mechanism that can automatically execute bundled scripts; the executable flag for every file must be hardcoded to False.
Furthermore, scenario validation is not a soft suggestion or ignorable warning but a mandatory precondition. The function must never silently complete missing scenarios or return a degraded result; it must raise an exception to explicitly reject input that does not satisfy the contract.
How it relates to neighboring concepts
Coordination of scenario validation, path sorting, and hash computation
Scenario validation is the first line of defense in generating the inventory, determining whether the function proceeds at all. Path sorting is the core determinism mechanism, guaranteeing stable output order by sorting file names lexicographically. Hash computation provides content-level verifiability for each file.
These three mechanisms work together: scenario validation intercepts incomplete packages, path sorting ensures deterministic output order, and the hash values and executable flags ensure content traceability and safety. Without scenario validation, even correct sorting and hashing would still produce an incomplete inventory.
Boundary decision for this stage: When the scenarios parameter does not equal {'software', 'office', 'music'}, the function must immediately raise a ValueError and must never enter the file traversal and inventory generation phase.
Complete the chapter
Package Forge requirement extraction as an installable Skill.
Local lab self-check
- Not started
- 2Reading
- 3Lab downloaded
- 4Test result read
- 5Local check passed
verification.json is parsed only in this browser and is never uploaded. This checks the lab contract; it is not server certification or third-party endorsement.
Concept calibration and one-week review
Three questions check the chapter's boundaries, delivery evidence, and recovery path. Answers stay in this browser.