SDK Runtime Boundaries
Tracker: pm-1eted6, pm-3lhth4, and pm-0xmajx, with forward history-epoch protection tracked by pm-aka8m7. Refusal reachability and recovery completeness are tracked by pm-elmpav, pm-185870, and pm-yqe0mo.
These SDK primitives keep host and project-runtime policy consistent across the bundled CLI, packages, and automation without requiring consumers to reproduce private CLI parsing rules.
Project runtime compatibility
Packages and automation can call inspectProjectRuntimeCompatibility before a
mutation, or assertProjectRuntimeCompatibility when refusal semantics are
preferred. The SDK discovers the strongest project-local pm version pin from
the package manifest, installed package metadata, and supported lockfiles. A
CLI older than that pin refuses mutation with
project_runtime_stale_mutation; read commands stay available so an agent can
recover context before upgrading. Stale reads, including context and
read-only health invocations, emit the non-blocking
project_runtime_stale_read warning. JSON modes write a single structured
warning object to stderr, leaving the command's normal stdout envelope valid;
human modes identify both versions, the redaction-safe pin source, and a
package-manager-neutral recovery action. SDK callers receive the same warning
inside ProjectRuntimeCompatibilityResult. PM_ALLOW_STALE_CLI=1 is the
explicit, auditable emergency override.
The boundary is bidirectional for history storage. A current runtime also
refuses a mutation when any package declaration or lockfile pin cannot select a
runtime that reads the writer epoch. Every manifest section is evaluated, so a
newer permissive declaration cannot hide an older exact pin. Upper-bounded
ranges that exclude the first compatible reader also fail closed, including
inclusive hyphen ranges. Disjunctive ranges evaluate each alternative, so one
branch that can select a compatible reader keeps the declaration writable. The
original incompatible declaration is preserved in the diagnostic rather than
being reduced to its representative lower bound. The typed
project_runtime_history_epoch_incompatible error reports the writer epoch,
the incompatible declaration, its redaction-safe source, and the minimum
upgrade. Reads remain available, and the same emergency override is reserved
for an intentionally coordinated fleet migration.
The public isProjectMutatingInvocation classifier applies the same decision
to package hosts and the bundled CLI. It resolves mixed command families by
their effective action: configuration, merge, schema, profile, package,
telemetry, workspace snapshot, template, VCS, validation, health, test, linked
artifact, and changelog reads remain available while their write forms are
fenced. Help, checks, previews, and dry runs remain reads, so compatibility
enforcement does not turn diagnostics into writes.
Host-environment fault boundary
Use withHostEnvironmentBoundary around filesystem and resource operations
that cross into the host. It translates recognized Node errno failures into
the stable, path-redacted host_environment_capacity_fault,
host_environment_permission_fault, or host_environment_resource_fault
contracts. classifyHostEnvironmentFault supports diagnostics that need a
non-throwing classification, while translateHostEnvironmentFault supports an
existing catch boundary. The classifier accepts both symbolic Node code
values and declared numeric errno values from node:os.constants.errno; this
also recognizes platform errors that Node renders only as a number, such as a
Linux -122 quota failure. Non-errno failures are returned unchanged and must
not be relabeled as environment faults.
Linked-test tracker seeding uses this boundary. A required sandbox copy that exceeds temporary-filesystem capacity fails with a stable recovery contract, while linked tests whose effective context is schema avoid materializing tracker data at all.
Package installation applies the same boundary to extension_install_copy,
extension_install_backup, extension_install_rollback, and
extension_module_graph_snapshot. These fixed operation labels identify the
failed stage without exposing source, destination, backup, or temporary paths.
Capacity and permission faults are not transient copy races and are not retried;
existing bounded retries for concurrent filesystem changes remain available.
The shared implementation lives under core/fs, while the public SDK exports
remain stable for package authors. This coverage does not imply that every
filesystem operation in the repository has adopted the boundary; the complete
migration and recurrence-gate obligation remains tracked in pm-3lhth4.
Existing SDK surfaces can supply category-specific codes to preserve their
published error vocabulary while still sharing classification, path redaction,
and recovery guidance. Workspace snapshots use this compatibility path for
their stable storage, resource, and permission fault codes.
Package archives use one bounded validation and extraction boundary whether
they come from a local path or npm pack. The SDK rejects links, escaping
paths, unsupported entry types, oversized archives, and decompression growth
before extraction. If npm reports an archive it did not create, callers receive
the path-redacted npm_package_archive_missing refusal instead of a raw system
tar exception; an archive reported outside the isolated pack destination is
rejected as npm_package_archive_unsafe. This keeps package install behavior
portable and prevents an untrusted registry artifact or package-manager result
from bypassing the local-archive policy.
CLI refusal ownership
CLI adapters preserve SDK error codes, exit semantics, and actionable recovery guidance when presenting refusals as human-readable or structured output. Host-only validation remains at the transport boundary, while rules shared by packages and commands live in public SDK primitives so callers receive the same refusal contract regardless of entrypoint.
createUnknownSubcommandError is the shared constructor for positional command
families. It emits unknown_subcommand with the stable
unknown_positional_token reason, a complete sorted allowed_values set, the
attempted command, and a nearest copy-pasteable retry when edit distance gives
an unambiguous candidate. CLI, direct SDK dispatch, MCP, and package hosts use
the same primitive. The CLI also recognizes split schema actions such as
schema add type Name and recommends the declared schema add-type Name
form instead of collapsing the failure into a generic arity error.
Core graph, config, plan, schema, profile, merge, telemetry, workspace, and
package/extension lifecycle dispatchers use this contract. The bundled
templates package demonstrates the same primitive for package-registered
families; custom packages can import it from the public SDK instead of
inventing a private refusal envelope. The former unknown_lifecycle_action
catalog name remains a compatibility alias of unknown_subcommand.
PmErrorCodeContract.owned_states declares concrete refusal states, their
probe ids, reachable entrypoints, and expected exit classes. The generated
catalog joins those declarations to the discovered error-code inventory.
Package and test harnesses can pass real-entrypoint observations to
verifyPmRefusalReachability; missing probes, wrong codes, wrong exit classes,
and undeclared observations fail closed. This makes an error code's existence
and its runtime reachability independently testable.
Unknown-option recovery separates human and machine budgets. Human guidance
shows the first three ranked command paths plus an explicit remainder count.
The structured envelope returns up to twelve ranked paths alongside
candidate_commands_total and candidate_commands_truncated, ordered by
shared option vocabulary and then command path. Consumers must inspect another
command contract before changing operations; candidate discovery is not an
instruction to run a different command.