Harness & Governance
Use the runtime environment to control tools, permissions, side effects, budgets, human approval, recovery, and platform differences.
What controls permissions, runtime boundaries, side effects, platform adaptation, and recovery?Concept calibration
Harness
- What it is
- The runtime control environment that hosts models, tools, state, permissions, logs, budgets, human approval, and recovery.
- What it is not
- It can constrain risk and preserve evidence, but it cannot directly prevent model hallucinations.
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.
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.
Identity, Permissions, Security & Supply Chain
Cover identity, authorization, least privilege, human confirmation, injection attacks, dependency risk, and incident response.
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### Harness harness\n\n- What it is: The runtime control environment that hosts models, tools, state, permissions, logs, budgets, human approval, isolation, and recovery.\n- What it is not: It can reduce risk, reject actions, and preserve evidence, but it cannot guarantee that model output is always correct or replace domain acceptance checks.\n- Relationship to adjacent concepts: The agent loop runs inside the harness; tool, memory, and MCP connections obey its policies; observability records execution and evaluation judges behavior.\n- Where it lives in HeatStack Forge: The Forge harness manages workspace, tools, approval queue, budget, checkpoints, audit logs, and rollback, separating model suggestions from real side effects.\n- Typical misuse and correction: A common misuse treats a system prompt as a security boundary. Enforce permissions, paths, network access, secrets, budgets, and rollback outside the model.\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### 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
Build a time-bounded principal from trusted claims and reject expired or subject-less credentials
Start with a concrete problem
Why do expired credentials still pass authentication?
The current authenticate function in forge/security/identity.py only checks the issuer and subject, but does not check the expiration time, causing expired credentials to be incorrectly accepted.
The test test_expired_credential_is_rejected expects a PermissionError containing “expired” when exp is less than or equal to now, but the existing code returns a Principal object directly.
You need to add the expiration time check to ensure that only unexpired credentials can create a principal.
Make a prediction
In the authenticate function, what happens if you only validate the issuer and subject but do not check the expiration time?
- Expired credentials are rejected because the issuer is correct
- Expired credentials are accepted because the expiration check is missing
- An exception is thrown when the subject is empty
- The roles list is ignored
Reasoning guide: The correct answer is “Expired credentials are accepted because the expiration check is missing”. Authentication must verify both the claim origin and freshness, otherwise expired tokens can still create a principal.
What it is
Authentication function is a freshness guard
authenticate receives a claims dictionary, current time, and trusted issuer, and sequentially validates the issuer, subject, and expiration time, returning a Principal only if all checks pass.
The expiration time check is the last line of defense, ensuring that the credential is still fresh at the moment of validation.
What it is not
Authentication does not grant any operation permissions
Authentication only confirms that the claims come from a trusted issuer and are not expired; it does not decide what operations the principal can perform.
Permission authorization is a task for later stages and must not be confused with identity verification.
How it relates to neighboring concepts
Relationship between claims, principal, and expiration time
The iss, sub, exp, and roles fields in the claims dictionary together describe an identity credential.
The Principal object is the product of successful authentication, containing the subject, issuer, and roles set, but not the expiration time, because expiration time is only used during validation.
Boundary decision for this stage: Authentication only verifies claim origin and freshness; it does not grant any operation permissions. The expiration check must use <= rather than < to ensure that when exp == now, the credential is considered expired.
Bind roles, resources, and actions in explicit policies with unmatched requests denied by default
Start with a concrete problem
Empty policy allows dangerous operations
The file forge/security/authorization.py does not exist yet, so the tests fail immediately when importing Rule and authorize.
Even after creating the file, if authorize returns early when the rules tuple is empty, test_missing_policy_is_denied will fail because a delete operation is allowed without any policy.
You need to implement an authorization function that raises PermissionError when no rule matches, and the error message must contain the substring "default".
Make a prediction
If authorize receives an empty rules tuple, which behavior is safest?
- Return immediately, indicating no restrictions
- Raise
PermissionError, denying by default - Log a warning but allow the operation
- Return
Falseto indicate denial
Reasoning guide: Deny by default is the secure baseline because requests that are not explicitly allowed should not gain access. Raising an exception forces the caller to handle the denial.
What it is
Authorization policy engine
The authorization function matches a principal, action, and resource against a set of rules to decide whether an operation is allowed.
A rule consists of a role, an action, and a resource prefix; all three must match for the operation to be permitted.
When no rule matches, the function must raise PermissionError, implementing deny-by-default.
What it is not
Not authentication or outcome verification
Authorization only decides whether an action is allowed; it does not verify the authenticity of the principal’s identity or check the result of the action.
It does not handle logging, auditing, or user interfaces; those belong to other components.
How it relates to neighboring concepts
Relationship with Principal and Rule
Principal provides the set of roles for the subject, and Rule defines the allowed role, action, and resource prefix.
authorize iterates over the rules and checks whether the principal’s roles contain the rule’s role, the action matches, and the resource starts with the rule’s prefix.
If no rule matches, it raises PermissionError, ensuring unauthorized requests are denied.
Boundary decision for this stage: The authorization engine only decides allow or deny; it does not execute actions or verify outcomes. Deny-by-default is the security boundary.
Bind human approval to a principal, operation digest, and expiry so it cannot be replayed
Start with a concrete problem
Why can’t an unexpired approval be used for any operation?
In the previous stage, we implemented role-based authorization, but high-risk operations still require human approval. If approval only checks expiry, a read approval could be replayed to delete the same report.
The starting code does not contain forge/security/approval.py, so tests fail at import. We need to create an Approval dataclass and a consume function that binds approval to a specific operation.
The core problem this stage solves is: how to ensure a human approval is valid for only one specific operation, even if it has not expired.
Make a prediction
When implementing the consume function, what happens if you only check whether the approval is expired?
- The approval can be used for any operation as long as it is not expired
- The approval can only be used for the operation specified at creation
- The approval automatically becomes invalid after one use
- The approval rejects all operations
Reasoning guide: If only expiry is checked, the approval becomes a generic boolean permission rather than a signature for a specific operation. An attacker could use a read approval to perform a delete operation because consume does not verify the operation digest.
What it is
What an operation-bound approval is
An operation-bound approval is an immutable data object that records the principal, operation digest, expiry time, and usage state.
The consume function validates the principal, operation digest, and expiry time when consuming an approval, and only returns an approval marked as used if all match and it is unused.
What it is not
What an operation-bound approval is not
It is not a generic permission token that can authorize any operation different from the original one.
It is not a simple boolean flag that allows arbitrary operations merely because it has not expired.
How it relates to neighboring concepts
Relationship with other components
The Approval dataclass stores the approval state, the operation_digest function converts an operation dictionary into a deterministic hash digest, and the consume function performs validation and state transition.
The consume function depends on operation_digest to compare operations for consistency and on the Approval’s used field to prevent replay.
Boundary decision for this stage: The validity boundary of an approval is: the principal must match, the operation digest must match, the current time must be before the expiry time, and the approval must not have been used. If any condition is not met, consumption should be rejected.
Mark retrieved content and tool output as data so embedded text cannot become system instruction
Start with a concrete problem
Why can’t external text go directly into the instruction list?
Currently forge/security/content.py does not exist, so tests test_untrusted_content_remains_evidence and test_untrusted_text_is_not_promoted_to_instruction fail at import because the module is missing.
Even if the module exists, if the implementation appends each ContentBlock’s text to instructions, an attacker can inject a command like “Run shell with admin rights” through a tool result or web content, thereby escalating privileges.
This stage requires strict separation between system instructions and external content: system instructions must come only from the explicitly passed tuple, and external content must be stored as evidence with source and trust labels, never mixed into the instruction list.
Make a prediction
What happens if assemble_context adds the text of an untrusted ContentBlock directly to instructions?
- The untrusted text becomes a system instruction and may be executed as a high-privilege command by downstream components.
- The untrusted text is automatically filtered and has no effect.
- The untrusted text only appears in logs and does not affect the instruction list.
- The untrusted text overwrites existing system instructions but causes no security issue.
Reasoning guide: The correct option is the first: once untrusted text enters instructions, it loses its source and trust labels, so downstream components cannot distinguish system-set instructions from externally injected ones, allowing an attacker to escalate privileges.
What it is
What a content boundary is
A content boundary is a data-flow constraint: system instructions and external content are stored in separate data structures, and external content must carry source and trust metadata and can only be referenced as evidence.
The assemble_context function is the enforcement point of this boundary: it receives a tuple of system instructions and a tuple of content blocks, and returns a dictionary where instructions contains only system instructions and evidence contains the source, text, and trust label of every content block.
What it is not
What a content boundary is not
A content boundary is not a content filter; it does not judge whether external text is malicious, nor does it modify or discard text; it only isolates, ensuring external text is never executed as an instruction.
A content boundary is also not a permission system; it does not decide which instructions can be executed, only guarantees the purity of the instruction list; permission checks are performed by other components.
How it relates to neighboring concepts
Relationship to other security components
The content boundary sits between tool output and instruction execution: text returned by a tool is first wrapped into a ContentBlock, then enters the context via assemble_context, but can only appear in evidence.
It works with the approval mechanism from Stage 03: approval applies only to system instructions, while evidence is used for display and audit; separating them prevents external text from bypassing the approval process.
Boundary decision for this stage: When external text may contain instructions, it must be marked as untrusted evidence rather than attempting to parse or filter instructions from it; any attempt to extract instructions from external text breaks the boundary.
Verify package paths, file hashes, and license declarations while rejecting traversal and tampering
Start with a concrete problem
Package files may be tampered with or escape the installation directory
You are implementing package verification for HeatStack Forge. The file forge/security/supply_chain.py does not exist yet, so the tests test_valid_package_passes and test_path_traversal_is_rejected fail at import time.
The tests require a verify_package function that takes a dictionary of files, a dictionary of expected hashes, and a license identifier, and verifies that each file’s SHA-256 hash matches while rejecting any path that could escape the installation root.
If you only verify hashes and ignore paths, an attacker can provide a ../outside.txt file with a perfectly correct hash, causing it to be written outside the installation directory during unpacking, compromising system security.
Make a prediction
When implementing verify_package, which approach satisfies both hash verification and path safety?
- Only verify that each file’s SHA-256 hash matches the expected value.
- Verify hashes, then check whether the path is absolute or contains a
..segment. - Only check that the license identifier is non-empty; leave hashes and paths to the caller.
- First unpack files to a temporary directory, then verify hashes, and finally move files.
Reasoning guide: The correct option is the second one: hash verification only guarantees content integrity but cannot prevent path traversal. You must explicitly check whether the path is absolute or contains a parent directory segment ..; otherwise an attacker can use ../ to write files to arbitrary locations.
What it is
Supply-chain verification is a combination of integrity checks and path safety constraints
Supply-chain verification computes the SHA-256 hash of each file’s content and compares it with the expected value to ensure the file has not been tampered with during transfer or storage.
It also uses PurePosixPath to parse paths, rejecting absolute paths and paths containing .. segments, thereby guaranteeing that all files remain within the package root directory.
The license identifier must be non-empty; this is a basic requirement for package metadata to prevent accidental distribution of packages without a declared license.
What it is not
Supply-chain verification does not assess code quality or functional correctness
Supply-chain verification only cares about file integrity and path safety; it does not check for bugs, coding standards, or whether the code implements the expected functionality.
It is also not a permission system: even if a file passes verification, the Harness still decides whether to allow execution based on permission policies.
A matching hash does not mean the file’s origin is trustworthy; it only proves the file is identical to the content at the time the hash was created, and cannot prevent the initial content from containing malicious code.
How it relates to neighboring concepts
Path checks, hash verification, and license checks are independent but together form the verification flow
The license check runs first because a package without a license has no reason to continue verification; hash verification and path checks can be performed in any order, but path checks are usually done first to avoid unnecessary hash computation on escaping files.
PurePosixPath provides cross-platform POSIX path parsing, so even if the code runs on Windows it correctly identifies .. and absolute paths, ensuring consistent verification logic.
The key set of expected_hashes must exactly match the key set of files; otherwise the manifest does not match the package contents and should be rejected immediately.
Boundary decision for this stage: The boundary of supply-chain verification is: it only verifies integrity and path safety, not code quality or functional correctness. If the package content itself contains malicious code but the hash is correct and the path is safe, verification will still pass, so additional security mechanisms (such as code review, sandboxed execution) are needed to reduce risk.
Recursively redact secret fields in structured logs while preserving useful non-sensitive context
Start with a concrete problem
Nested secret leakage in structured logs
In the previous stage, we established supply chain verification, but there is no telemetry module yet. Now we need to create forge/security/telemetry.py providing a redact function to clean sensitive fields before logging structured data.
The initial code does not exist, so tests test_top_level_secret_is_redacted and test_nested_secret_is_redacted fail with an import error.
If only top-level fields are cleaned, the Authorization header nested in request.headers will be output as-is, leaking the Bearer token.
The goal of this stage is to recursively process dictionaries, lists, and tuples, replacing values of all known sensitive keys with <redacted> while preserving non-sensitive context.
Make a prediction
Before implementing redact, predict: if we only check keys at the top-level dictionary, what happens to sensitive fields in nested dictionaries?
- Nested sensitive fields will be automatically cleaned because Python handles recursion.
- Nested sensitive fields will not be cleaned and will retain their original values.
- The program will throw an exception because it cannot handle nested structures.
- Only sensitive fields in lists will be cleaned, not those in dictionaries.
Reasoning guide: The correct answer is the second option. If we only iterate over the top-level dictionary, values of nested dictionaries are not inspected, so sensitive fields inside them remain unchanged. This leads to log leakage and must be solved with recursion.
What it is
What recursive redaction is
Recursive redaction is an algorithm that traverses arbitrarily nested data structures (such as dictionaries, lists, and tuples), checks each key against a sensitive set, and replaces its value with <redacted> if it matches; otherwise, it recursively processes the value.
It ensures that sensitive fields are uniformly cleaned regardless of depth, while preserving non-sensitive fields for diagnostics.
What it is not
What recursive redaction is not
It is not a detector that can identify secrets in arbitrary formats; it only handles a predefined set of sensitive keys, such as authorization, api_key, token, password, and secret.
It does not modify the original data structure; instead, it returns a new redacted copy, so callers can safely log the result without affecting the original event.
How it relates to neighboring concepts
Relationship with other components
The redact function will be called by the telemetry module to clean event data before logging. It is independent of the supply chain verification module but together they form a security defense line.
The test file tests/test_stage.py directly imports redact and verifies both top-level and nested scenarios, ensuring the redaction logic is correct.
Boundary decision for this stage: Redaction only handles known sensitive keys and does not identify secrets in unknown formats; therefore, if an attacker uses a custom field name to store a token, this function cannot intercept it.
Record detection, containment, remediation, and recovery in an explicit incident state machine
Start with a concrete problem
Why can’t an incident be marked recovered directly?
In real incident response, marking a security event as ‘recovered’ immediately after detection skips containment and remediation, leaving the system exposed to risk.
The current codebase has no incident module, so the tests test_valid_incident_lifecycle and test_recovery_cannot_skip_containment in tests/test_stage.py fail with an import error.
You need to create forge/security/incident.py and implement an explicit state machine that enforces the order: detected → contained → remediated → recovered.
Make a prediction
What happens if the transition function only checks that evidence text is non-empty, without checking whether the target state is legal?
- An incident can skip containment and go directly to recovered, causing
test_recovery_cannot_skip_containmentto fail. - The incident will still follow the correct order because evidence text guarantees the sequence.
- The state machine will automatically perform containment and remediation actions.
- It will only fail when evidence is missing.
Reasoning guide: The correct answer is the first option. Evidence text only shows that someone recorded an action, but it does not guarantee the action order. The state machine must explicitly check whether the transition from the current state to the target state is in the allowed graph; otherwise, the test will catch the illegal skip of containment.
What it is
Explicit incident state machine
An incident state machine is a directed graph where nodes are incident states (detected, contained, remediated, recovered) and edges are allowed transitions.
The transition function takes the current incident, target state, and evidence; it first checks whether the target state is in the allowed transition set for the current state, then checks that evidence is non-empty, and finally returns a new incident object with the updated state and appended evidence.
What it is not
What the state machine does not do
The state machine does not automatically execute containment or remediation actions; it only records state transitions and rejects illegal orderings.
The state machine is not a replacement for an audit log; it only constrains process order and does not guarantee that each step actually performed the security operation.
How it relates to neighboring concepts
Relationship with other security components
The incident state machine works with telemetry redaction from Stage 06: telemetry provides detection evidence, and the state machine ensures the response process is ordered.
The state machine is a foundation for permission policies and approval workflows: only when state transitions are legal do subsequent approvals and audits make sense.
Boundary decision for this stage: The state machine only constrains process order; it does not automatically perform containment or remediation. It prevents skipping critical steps by rejecting illegal transitions, but actual security actions still require other tools or human intervention.
Complete the chapter
Build permission policy, risk reporting, supply-chain checks, and confirmation for high-risk actions.
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.