diff --git a/README.md b/README.md index 856bf529d..8acb81b35 100644 --- a/README.md +++ b/README.md @@ -316,6 +316,8 @@ Privacy information can be found at https://privacy.microsoft.com and in the Mic | [docs/host-prep.md](docs/host-prep.md) | Windows host preparation (`wxc-host-prep.exe`) | | [docs/diagnostics.md](docs/diagnostics.md) | Diagnostic logging and ETW | | [docs/sandbox-policy/0.7.0/policy.md](docs/sandbox-policy/0.7.0/policy.md) | Sandbox policy 0.7.0 specification | +| [docs/mxc-policy-store-api.md](docs/mxc-policy-store-api.md) | Policy Store API contract: types, behavior, and caller examples (under review) | +| [docs/mxc-policy-store.md](docs/mxc-policy-store.md) | Policy Store catalog design, authoring, and implementation | | [docs/process-container/guide.md](docs/process-container/guide.md) | Windows AppContainer / BaseContainer guide | | [docs/lxc-support/lxc-backend.md](docs/lxc-support/lxc-backend.md) | LXC backend (Linux) | | [docs/bwrap-support/bubblewrap-backend.md](docs/bwrap-support/bubblewrap-backend.md) | Bubblewrap backend (Linux) | diff --git a/docs/mxc-policy-store-api.md b/docs/mxc-policy-store-api.md new file mode 100644 index 000000000..a2aec5716 --- /dev/null +++ b/docs/mxc-policy-store-api.md @@ -0,0 +1,731 @@ +# Policy Store API + +**Status:** Under review. Pending MXC SDK API sign-off before implementation. +Not part of MXC 1.0; no later release is committed. + +This document defines the caller-facing contract. The +[design spec](mxc-policy-store.md) owns catalog authoring, implementation, +validation, and release mechanics. + +Policy Store resolves suggested requirements for one tool or a set of tools +before the execution command is known. Results are best-effort floors, not +authorization or guarantees of success or safety. Clients and users review +the requested access; their restrictions and enterprise policy take precedence. +Lookup never executes a tool, creates a container, downloads catalog data, or +writes consumer state. + +## 1. Operations + +| Operation | Result | Use | +|---|---|---| +| `resolveToolRequirements` | `ContainerRequirements` or absence | Resolve one tool or compose several tools' requirements | +| `resolveToolRequirementsWithDiagnostics` | Requirements plus diagnostics | Also inspect coverage, selections, dependencies, and warnings | +| `listCatalogEntries` | Metadata array | Inspect entries without resolving policy bodies | +| `getCatalogInfo` | Catalog/version metadata | Identify the installed default revision | + +TypeScript signatures below define the shared contract. All named public +types and operations are exported from `@microsoft/mxc-sdk/v1`. + +```ts +export declare function resolveToolRequirements( + tool: ToolInput, ctx?: ResolveContext, +): Promise; +export declare function resolveToolRequirements( + tools: readonly ToolInput[], ctx?: ResolveContext, +): Promise; + +export declare function resolveToolRequirementsWithDiagnostics( + tool: ToolInput, ctx?: ResolveContext, +): Promise; +export declare function resolveToolRequirementsWithDiagnostics( + tools: readonly ToolInput[], ctx?: ResolveContext, +): Promise; + +export declare function listCatalogEntries(): CatalogEntryMetadata[]; +export declare function getCatalogInfo(): { + catalogSchemaVersion: string; + catalogRevision: string; + sdkContractVersion: string; +}; +``` + +Resolution is asynchronous in Node, with plain names and no `Async` suffix; +it must not block the event loop. Inspection is synchronous. + +| SDK | Resolution operations | Return convention | +|---|---|---| +| Rust `mxc_sdk::v1` | `resolve_tool_requirements`, `resolve_tool_requirements_with_diagnostics` | `Result, Error>` or `Result` | +| .NET `Microsoft.Mxc.Sdk.V1` | `MxcContainer.ResolveToolRequirements`, `ResolveToolRequirementsWithDiagnostics`; corresponding `Async` forms | Nullable requirements or diagnostic result; asynchronous forms return Tasks | +| Node `@microsoft/mxc-sdk/v1` | Signatures above | Promises; absence is `undefined` | + +Rust uses an idiomatic one-or-many input rather than overloads. A single-tool +call equals a one-element array. Both resolution operations produce the same +requirements; diagnostics come from that pass, not a second lookup or global +"last result." Metadata inspection exposes selectors and provenance, not policy +bodies, and reports the installed default catalog. + +## 2. Types and fields + +### Requirements and inputs + +```ts +import type { ContainerRequest, NetworkRuleConfig } from "@microsoft/mxc-sdk/v1"; + +export type ContainerRequirements = Pick; + +export interface ToolCandidate { + invocationName: string; + packageUrl?: string; + detectedVersion?: string; + intent?: string; +} + +export type ToolInput = string | ToolCandidate; +export type CatalogPlatform = "windows" | "linux" | "macos"; +export type CatalogArchitecture = "x64" | "arm64"; + +export interface PlatformVariantSelector { + platform: CatalogPlatform; + architecture?: CatalogArchitecture; +} + +export interface ResolveContext { + projectRoot?: string; + symbols?: Record; + platform?: CatalogPlatform; + architecture?: CatalogArchitecture; + catalogRevision?: string; + allowWeakIdentityFallback?: boolean; +} +``` + +`ContainerRequirements` reuses the SDK's existing nested types and optionality. +Rust/.NET use equivalent four-field aggregates, not duplicate nested models. +The resolver returns only catalog-supported fields: + +| Field | Supported content | +|---|---| +| `filesystem` | `readonlyPaths`, `readwritePaths`, `deniedPaths` | +| `network` | Directional `egress` and `ingress` policy | +| `ui` | Required `disable` when UI is present; optional `clipboard` and `allowInputInjection` | +| `timeoutMs` | Unsigned 32-bit integer | + +`Pick` is not runtime validation or permission to return other nested settings. +Commands, containment, name, working directory, environment, lifecycle/cleanup, +runtime proxy configuration, telemetry and execution options remain caller-owned. +No command is needed for lookup. Intent selects requirements, not a command. + +A string input means `{ invocationName: tool }`, with no package/version/intent +evidence. The library does not verify an installed tool's identity or discover +its version from a supplied package URL. + +### Results and selections + +`IntentSelection.mode` reports how intent requirements were selected; it is not +a caller option. Effective requirements reflect the applicable +[version/platform selection](#version-intent-and-platform-selection) and +[dependency rules](#dependencies-and-composition). + +| Mode | When reported | Requirements contributed | `selected` | +|---|---|---|---| +| `named` | The caller requested a supported intent, or a dependency reference explicitly named supported intents | Effective base plus only the named intents' additions | Selected intent names, sorted | +| `all` | A directly requested tool omitted `intent` | Effective base plus all effective intents; the base still contributes when no intents are defined | All effective intent names, sorted; `[]` when none exist | +| `none` | A dependency reference did not name intents | Applicable dependency base only, without intent additions | `[]` | +| `unsupported` | The requested intent is unavailable for the selected tool/version/platform | Nothing from that tool/intent pair; status is `intent_unsupported`, with no base/all-intents fallback | `[]` | + +An unparseable version skips intent resolution. + +```ts +export interface IntentSelection { + requested?: string; + mode: "named" | "all" | "none" | "unsupported"; + selected: string[]; +} + +export type VersionStatus = + | "matched_default" + | "matched_version" + | "version_out_of_range" + | "version_unparseable"; + +export interface VersionSelection { + status: VersionStatus; + detectedVersion?: string; + selectedVersionRange?: string; +} + +export type ToolResolutionStatus = + | VersionStatus + | "intent_unsupported" + | "tool_unmatched" + | "filesystem_identity_unresolved"; + +export interface ToolRequirementsResolution { + requirements: ContainerRequirements | undefined; + diagnostics: { + catalogRevision: string; + tools: Array<{ + inputIndex: number; + status: ToolResolutionStatus; + matches: Array<{ + entryId: string; + entryRevision: number; + matchedIdentities: Array<{ + kind: string; + strength: "strong" | "weak"; + }>; + versionSelection: VersionSelection; + intentSelection?: IntentSelection; + }>; + }>; + resolvedDependencies: Array<{ + entryId: string; + entryRevision: number; + inputIndexes: number[]; + requiredVersionRange?: string; + versionSelection: VersionSelection; + intentSelection: IntentSelection; + }>; + warnings: PolicyResolutionWarning[]; + }; +} +``` + +### Warnings + +Each warning has a stable `code`, category-specific fields, and a human +`message`. Consumers use fields, not message parsing. + +```ts +export type InputWarning = { inputIndex: number; message: string }; +export type ToolResolutionWarning = InputWarning & ( + | { code: "version_out_of_range" | "version_unparseable"; + entryId: string; detectedVersion: string } + | { code: "intent_unsupported"; entryId: string; intent: string } + | { code: "tool_unmatched"; invocationName: string } + | { code: "purl_invalid"; packageUrl: string } + | { code: "purl_components_ignored"; packageUrl: string; + ignoredComponents: Array<"version" | "qualifiers" | "subpath"> } + | { code: "weak_identity"; entryId: string; invocationName: string } +); + +export interface WarningScope { + inputIndexes: number[]; + entryIds: string[]; + message: string; +} + +export interface PathRequirement { + path: string; + access: "denied" | "readonly" | "readwrite"; + entryIds: string[]; +} + +export type EgressRule = NetworkRuleConfig; + +export interface NetworkRequirement { + rule: EgressRule; + entryIds: string[]; +} + +export type ResolutionDetailWarning = WarningScope & ( + | { code: "architecture_default"; platform: CatalogPlatform; + architecture: CatalogArchitecture } + | { code: "architecture_fallback"; platform: CatalogPlatform; + architecture: CatalogArchitecture; selected: "platform" | "default" } + | { code: "symbol_resolved"; symbol: string; value: string; + source: "caller" | "discovery" | "host" | "default" } + | { code: "symbol_unresolved"; symbol: string } + | { code: "filesystem_case_assumed"; paths: string[]; + comparison: "case_sensitive" } + | { code: "filesystem_identity_unresolved"; paths: string[]; + platform: CatalogPlatform } + | { code: "readonly_superseded"; removed: PathRequirement; + requiredBy: PathRequirement[] } + | { code: "filesystem_deny_removed"; removed: PathRequirement; + requiredBy: PathRequirement[] } + | { code: "network_deny_removed"; removed: NetworkRequirement; + requiredBy: NetworkRequirement[] } +); + +export type PolicyResolutionWarning = ToolResolutionWarning | ResolutionDetailWarning; +``` + +Warnings about shared contributions carry sorted, distinct `inputIndexes` and +`entryIds`. Each path/rule identifies its source entries. `removed` contains +the full original path or egress rule, including exclusions/protocol/ports, +not just the overlap. Other grants may apply throughout the removed scope. + +### Inspection metadata + +```ts +export type CatalogIdentityMetadata = + | { kind: "purl"; value: string } + | { kind: "invocation-name"; names: string[] }; + +export interface CatalogIntentMetadata { + name: string; + exampleSubcommands?: string[]; + dependencyEntryIds: string[]; +} + +export interface CatalogAdditionsMetadata { + dependencyEntryIds: string[]; + intentAdditions: CatalogIntentMetadata[]; + newIntents: CatalogIntentMetadata[]; +} + +export interface CatalogEntryMetadata { + catalogRevision: string; + entryId: string; + entryRevision: number; + displayName: string; + versionScheme: "npm" | "semver" | "pypi" | "nuget" | "intdot"; + identity: CatalogIdentityMetadata[]; + default: { + dependencyEntryIds: string[]; + intents: CatalogIntentMetadata[]; + }; + platformVariants: Array; + versionVariants: Array; + provenance: { + method: string; + sourceRevision: string; + }; +} +``` + +## 3. Behavior + +### Context defaults and symbol resolution + +| Omitted input | Behavior | +|---|---| +| `ctx` | No caller overrides; lookup still requires an explicit API call | +| `platform` | Current host platform | +| `architecture` | Native system architecture, not the library process's architecture | +| `catalogRevision` | Installed default revision | +| `allowWeakIdentityFallback` | `false` | +| `projectRoot`, `symbols` | No caller path overrides; no invented project root | +| `packageUrl`, `detectedVersion` | No fabricated identity/version evidence | +| `intent` | All intents of the effective policy, not of other version variants | + +Only symbols required by selected entries and dependencies are resolved: +caller value first, supported local discovery second, documented platform +default third, otherwise unresolved. Discovered/defaulted values and their +sources appear in diagnostics. A configuration-read failure is a library error, +not permission to assume a default. An unresolved required symbol prevents +requirements from being returned; it is not silently dropped. + +Discovery describes the current host/environment. Callers targeting another +environment supply overrides. Re-resolve or supply appropriate values if the +eventual execution environment differs. The catalog does not run tools to +discover locations or versions. Definitions of supported symbols belong to the +[catalog contract](mxc-policy-store.md#42-entry-shape). + +Only bundled revisions are selectable. An unavailable explicit revision fails, +without downloading or substituting another revision. Installing an SDK update +does not rewrite requirements previously accepted by a consumer. + +### Identity and match precedence + +Lookup uses identity, optional detected version, and optional tool-defined +intent. Raw command lines and calling-application identity are not keys. +Callers map operations to intent names; catalog subcommand examples are hints, +not parsing rules. A caller-supplied identity is not verified identity. + +Parse complete PURLs using the pinned +[component rules](https://github.com/package-url/purl-spec/blob/7cd2d3442fb9c88155db17ada7c911b40ec22d41/docs/specification/standard/Clause-5-Package-URL-Specification.md) +and [type definitions](https://github.com/package-url/purl-spec/tree/7cd2d3442fb9c88155db17ada7c911b40ec22d41/types), +including percent-decoding before comparison. Compare only type, namespace, +and name. Type is case-insensitive; namespace/name use their type's rules, +or exact decoded comparison where no special rule exists, never host casing. +Candidate version/qualifiers/subpath are ignored with `purl_components_ignored`. +Only `detectedVersion` supplies version evidence. Invalid PURLs produce +`tool_unmatched` with `purl_invalid` for that pair, without fuzzy repair or +invocation-name retry. Invalid catalog PURLs are authoring errors. + +Invocation names compare case-insensitively and locale-independently on +Windows/macOS, and exactly on Linux. Name-only matching requires +`allowWeakIdentityFallback: true`. +For each tool, rank eligible matches in this order: + +1. Package URL over invocation-name-only identity. +2. Requested intent declared in the default/applicable overlays over no such declaration. +3. Exact architecture over platform-neutral additions or common default. + +One highest-ranked match wins. Distinct ties fail with `ambiguous_match`; +multiple predicates in one entry count as one match at the strongest satisfied +strength. Version ranges do not select a different tool entry. A subsequent +version/intent failure does not retry a weaker entry or wildcard. + +### Version, intent, and platform selection + +Each entry has one common default and optional additive platform/architecture +and version overlays. Select at most one overlay of each kind; version ranges +do not overlap or cascade. Parse `detectedVersion` using the entry's scheme, +without fuzzy repair or trying other schemes. + +| Version input | Effective requirements | Version status | +|---|---|---| +| Omitted | Default + platform additions | `matched_default`; no version warning | +| In one range | Default + platform + that version's additions | `matched_version`; record selected range | +| Valid but in no range | Default + platform additions | `version_out_of_range` warning; never nearest/highest/broadest variant | +| Unparseable | No contribution from the pair | `version_unparseable` warning | + +Ranges use [VERS](https://github.com/package-url/vers-spec/blob/797c842a4afebf258e6710a68cd60306afc36708/docs/specification/standard/Clause-5-VERS-Specification.md) +with the pinned [version types](https://github.com/package-url/vers-spec/blob/797c842a4afebf258e6710a68cd60306afc36708/docs/types/vers-types.md): +`npm` (node-semver), `semver` (SemVer 2.0.0), `pypi` (PEP 440), +`nuget` (NuGet rules), and `intdot` (dotted integers). The type governs +normalization, prefixes and prereleases; do not independently strip prefixes +or apply npm range rules to other types. `generic` is unsupported. + +Intent behavior is defined by the [selection-mode table](#results-and-selections). +Out-of-range version plus newer-only intent emits both warnings. + +After identity/intent specificity, exact architecture additions beat +platform-only additions; otherwise retain the common default. Never select +another architecture. Architecture detection failure is an error. +Host-derived selection does not establish the installed tool's architecture: +an x64 tool on ARM64 may require an explicit x64 selection. Diagnostics report +host-derived selection and neutral/default fallback, not identity verification. + +### Dependencies and composition + +Compose contributing tools into one shared requirements object. A dependency +uses its unversioned base plus applicable platform base additions, with intent +additions selected by the [selection-mode table](#results-and-selections). +It does not select a version overlay; a dependency range is not detected +version evidence. +Resolution is transitive and cycle-rejecting. Shared base/platform layers +contribute once; distinct selected version/intent additions remain. Per-input +attribution, including transitive dependency requesters, is retained. + +Composition combines needed access, not caller authorization: + +| Selected filesystem requirements | Result | +|---|---| +| Read-only and read-write at the same path | Read-write | +| Read-write parent and read-only child | Read-write parent; omit redundant read-only child | +| Read-only parent and read-write child | Keep both; do not make the whole parent writable | +| Catalog deny overlapping a required grant | Remove the whole deny; report its full scope | +| Non-overlapping deny and grant | Keep both | + +Emit `readonly_superseded` when read-write requirements supersede a read-only +requirement, identifying the paths, access classes, and contributing entries. + +Rules apply after symbol substitution to equal/nested paths and established +object aliases. Keep required alias pathnames accessible, including read-only +aliases of an object another tool needs read-write. Use actual filesystem +case rules; if unknown, preserve case for comparison and report the assumption, +but never use lexical comparison as a substitute for necessary object checks. +Lookup inspects local host-side source paths, not remote/guest filesystems. +Unknown necessary identity excludes affected pairs with +`filesystem_identity_unresolved`; never retain a grant by dropping only an +unresolved restrictive side. Runner enforcement remains authoritative. + +A no-network tool does not veto another tool's network requirement. One +network-requiring component retains its supported settings. Multiple scoped +egress requirements union whole allow/deny rules under deny-by-default, +preserving destination/exclusion/protocol/port pairings. Remove an entire catalog +deny that overlaps a required allow after CIDR exclusions and protocol/port +intersection; retain other denies. Report removed rules and full affected scope. +Do not introduce unrestricted access or unselected overlay/intent additions. + +Allow-by-default, non-default ingress, proxy, UI, timeout, and other fields +without composition rules fail when distinct selected policies require an +undefined merge. Caller-owned restrictions are never removed. Access belongs +to the combined sandbox, not separate permissions for each tool. + +### Results, coverage, and attribution + +**Both resolution APIs intentionally allow partial results.** A non-absent +requirements-only result makes no coverage promise. Callers needing coverage +use diagnostics; there is no `requireAllMatches` option. + +`tool_unmatched`, `version_unparseable`, `intent_unsupported`, and +`filesystem_identity_unresolved` pairs contribute nothing; independent valid +pairs still contribute. No wildcard fills a missing match. Empty/all-noncontributing +input yields absence, not an empty requirements object. Missing required symbols +in otherwise selected requirements prevent output rather than silently dropping +those requirements. Library failures remain distinct from these outcomes. + +Diagnostic tool records follow input order. `matches` has at most one selected +entry. Unmatched inputs have an empty list and a warning: `purl_invalid` for an +invalid candidate PURL, otherwise `tool_unmatched`. Version/intent failures can retain +identity attribution without contributing requirements: + +- `versionSelection` records supplied version and status; `selectedVersionRange` + appears only for `matched_version`. +- A contributing pair's status is its version status. Unsupported intent or + unresolved identity replaces the pair status while preserving completed + selection metadata. Out-of-range plus unsupported intent emits warnings in + that order. Structured per-input warnings preserve input order. +- Dependencies report `matched_default`. + Deduplicate identical entry/revision/range/selection records, + sorted by those fields, unioning sorted distinct direct/transitive `inputIndexes`. + +## 4. Errors and consumer responsibilities + +Use the existing MXC primary code for ordinary handling; `details.reason` is +optional extra detail. When supplied, it uses the mapping below. Unknown/absent +reasons retain primary-code handling; callers need not parse messages. + +| Failure | MXC code | Optional reason | +|---|---|---| +| Invalid tool input or context | `malformed_request` | `invalid_context` | +| Invalid catalog, missing dependencies, or cycles | `policy_validation` | `invalid_catalog` | +| Tied highest-ranked matches | `policy_validation` | `ambiguous_match` | +| Incompatible SDK target or undefined composition | `policy_validation` | `composition_conflict` | +| Unsupported/undetectable host platform or architecture | `unsupported_containment` | `unsupported_host` | +| Bundled catalog cannot be read | `backend_error` | `integrity` | +| Explicit revision is not installed | `backend_error` | `revision_unavailable` | + +Invalid candidate PURLs, unparseable version strings, unsupported intents, and +unresolved filesystem identity are per-pair outcomes, not whole-call failures. +Supported filesystem/network overlaps are resolved, not composition failures. + +The consumer controls lookup enablement, acceptance, authorization/elevation, +and execution. Keep catalog requirements separate from user/learned policies; +preserve OS, enterprise, device, and backend ceilings. Failure to realize a +required floor never authorizes uncontained execution. When persisting or +auditing accepted requirements, retain catalog/entry revisions, contributing +inputs, warnings, and approval state. The library never mutates that state. + +## 5. Usage examples + +Zava Agent is an illustrative agent client. Its requirements advisor queries +the read-only MXC catalog; it does not manage catalog files. Git versions, +intents, and paths below are examples, not a promised production catalog. + +### Zava Agent: create a tool request with diagnostics + +Zava Agent first checks its own saved settings for the current tool and intent. +Only when none apply and catalog lookup is enabled does it request an MXC floor. +The application hooks below are scoped to the current user, workspace, and target; +the saved-settings helper checks tool identity/version and context before reuse. + +```ts +// zava-agent-requirements.ts +import { + resolveToolRequirementsWithDiagnostics, getCatalogInfo, listCatalogEntries, + MxcError, +} from "@microsoft/mxc-sdk/v1"; +import type { + ContainerRequirements, ContainerRequest, ResolveContext, ToolCandidate, + PolicyResolutionWarning, ToolRequirementsResolution, CatalogEntryMetadata, +} from "@microsoft/mxc-sdk/v1"; + +// Zava Agent-owned settings, logging and error hooks; implementations omitted. +export interface ZavaAgentApp { + loadApplicableToolRequirements( + tool: ToolCandidate, context: ResolveContext, + ): Promise; + isMxcFloorCatalogEnabled(): boolean; + logResolution(tool: ToolCandidate, diagnostics: ToolRequirementsResolution["diagnostics"]): void; + showUnpreparedTool(tool: ToolCandidate, diagnostics: ToolRequirementsResolution["diagnostics"]): void; + // Applies tool settings and client-wide restrictions, including "never allow" paths. + // May prompt if configured; returns chosen requirements or undefined to stop. + applyClientSettings( + tool: ToolCandidate, requirements: ContainerRequirements, + warnings: PolicyResolutionWarning[], + ): Promise; + logLibraryFailure(error: MxcError): void; + showInputCorrection(error: MxcError): void; + showUnsupportedHost(error: MxcError): void; + showCatalogUnavailable(error: MxcError): void; +} + +export class ZavaAgentRequirements { + constructor( + private readonly app: ZavaAgentApp, + private readonly context: ResolveContext = {}, + ) {} + + inspectCatalog(): { + info: ReturnType; entries: CatalogEntryMetadata[]; + } | undefined { + try { + return { info: getCatalogInfo(), entries: listCatalogEntries() }; + } catch (error) { + this.reportFailure(error); + return; + } + } + + private async prepareTool(tool: ToolCandidate): Promise { + const saved = await this.app.loadApplicableToolRequirements(tool, this.context); + if (saved !== undefined) return this.app.applyClientSettings(tool, saved, []); + if (!this.app.isMxcFloorCatalogEnabled()) return undefined; + + let result: ToolRequirementsResolution; + try { + result = await resolveToolRequirementsWithDiagnostics(tool, this.context); + } catch (error) { + this.reportFailure(error); + return undefined; + } + + // Keep warning fields, selections, and dependency attribution in the audit record. + this.app.logResolution(tool, result.diagnostics); + const status = result.diagnostics.tools[0]?.status; + const covered = status === "matched_default" || status === "matched_version" + || status === "version_out_of_range"; + if (result.requirements === undefined || !covered) { + this.app.showUnpreparedTool(tool, result.diagnostics); + return undefined; + } + + // Path discovery is logged; client settings decide how other warnings affect use. + const relevantWarnings = result.diagnostics.warnings.filter( + warning => warning.code !== "symbol_resolved"); + return this.app.applyClientSettings(tool, result.requirements, relevantWarnings); + } + + async createRequestForTool( + tool: ToolCandidate, command: string, + ): Promise { + const requirements = await this.prepareTool(tool); + return requirements === undefined ? undefined : { ...requirements, command }; + } + + private reportFailure(error: unknown): void { + if (!(error instanceof MxcError)) throw error; + this.app.logLibraryFailure(error); // Include code, message, and details in app logs. + switch (error.code) { + case "malformed_request": + this.app.showInputCorrection(error); + break; + case "unsupported_containment": + this.app.showUnsupportedHost(error); + break; + case "policy_validation": + case "backend_error": + this.app.showCatalogUnavailable(error); + break; + default: + throw error; + } + } +} +``` + +This client logs every diagnostic; it changes workflow only where needed: + +| Diagnostic | Client behavior | +|---|---| +| `symbol_resolved`; successful default/version selections | Log; use the result according to client settings | +| Architecture defaults/fallbacks, assumed casing, ignored PURL components, weak identity, out-of-range version | Pass the uncertainty to `applyClientSettings` | +| Superseded read-only access or removed filesystem/network denies | Pass the full changed scope and source entries to the same helper | +| Unmatched/invalid identity, unparseable version, unsupported intent, unresolved filesystem identity or required symbol | Return no prepared requirements; show corrective context | +| Library failure | Report by primary MXC code; no retry, revision substitution, or uncontained fallback | + +The enablement check is a Zava Agent preference, not an MXC API. Saved +tool/intent settings take precedence even when catalog lookup is disabled. +`applyClientSettings` may accept requirements unchanged, further restrict them, +or decline them, including applying client-wide restrictions to saved settings. +The client can allow automatic use or require a prompt; this +example specifies neither UI nor persistence policy. Any decision uses the +structured facts rather than parsing warning messages. These helpers do not +write to the MXC catalog. The caller can also modify its final `ContainerRequest` +before submitting it to MXC; catalog recommendations never override its settings. + +With no applicable saved settings, a disabled catalog or an unresolved/rejected +suggestion leaves this sample's action unprepared. The caller does not create +an empty or uncontained fallback. Application-helper failures propagate to the +application error handler rather than being disguised as an absent catalog match. + +The caller supplies workspace and target context when constructing the advisor, +then calls `createRequestForTool(tool, command)` for an execution. Its private +`prepareTool` helper obtains requirements and applies client settings. The command +is added only to the returned `ContainerRequest`, never sent to the catalog API. +Catalog inspection is optional; none of these methods creates a container. + +```ts +// Configure the advisor for this workspace, then prepare a Git push request. +declare const app: ZavaAgentApp; +const advisor = new ZavaAgentRequirements(app, { + platform: "windows", architecture: "x64", allowWeakIdentityFallback: true, + projectRoot: String.raw`D:\work\repo`, + symbols: { + git_prefix: String.raw`D:\tools\git`, + ssh_prefix: String.raw`D:\tools\ssh`, + node_prefix: String.raw`D:\tools\node`, + programData: String.raw`C:\ProgramData`, + temp_dir: String.raw`D:\temp`, + }, +}); +const catalogMetadata = advisor.inspectCatalog(); // Optional inspection; no UI is prescribed. +const gitRequest = await advisor.createRequestForTool({ + invocationName: "git", detectedVersion: "2.45", intent: "push", +}, "git push"); +// Only a defined request goes to Zava Agent's normal MXC run/spawn path. +``` + +With the [design's Git fixture](mxc-policy-store.md#42-entry-shape), requesting +`bundle-fetch` without a version, saved settings, or a defined default intent +returns `undefined` when lookup is enabled; no broader intent is substituted. +Git `2.45` + `push` records the SSH dependency in diagnostics. Multiple callers +share the immutable MXC catalog, not their saved settings or approval state. + +### A smaller caller: best-effort build workspace without diagnostics + +A project runner prepares one shared sandbox for a Node build that invokes Git. +It uses the catalog as a best-effort starting configuration and applies its own +settings, without needing per-tool match details. This Windows example prepares +the request; the runner's normal MXC execution path runs the build. + +```ts +import { resolveToolRequirements } from "@microsoft/mxc-sdk/v1"; +import type { ContainerRequirements, ContainerRequest, ResolveContext } from "@microsoft/mxc-sdk/v1"; + +// Application helpers; automatic use, prompting, and persistence are client choices. +declare function applySharedClientSettings( + requirements: ContainerRequirements, +): Promise; +declare function showNoSharedSuggestion(): void; + +async function prepareSharedWorkspace( + context: ResolveContext, +): Promise { + const suggestion = await resolveToolRequirements(["git", "node"], { + ...context, allowWeakIdentityFallback: true, + }); + if (suggestion === undefined) { + showNoSharedSuggestion(); + return undefined; + } + // Non-empty is not proof that both tools matched. + return applySharedClientSettings(suggestion); +} + +// The runner supplies installed-tool paths from its own configuration or discovery. +declare const installedToolPaths: ResolveContext["symbols"]; +const projectRoot = String.raw`D:\work\repo`; +const selectedRequirements = await prepareSharedWorkspace({ + projectRoot, + symbols: installedToolPaths, +}); +const buildRequest: ContainerRequest | undefined = selectedRequirements === undefined + ? undefined + : { ...selectedRequirements, command: "node build.js", workingDirectory: projectRoot }; +// Only a defined request goes to the runner's normal MXC execution path. +``` + +The short `["git", "node"]` input deliberately opts into name-only matching, +uses common defaults without detected versions, and includes all effective +intents. This is a broad starting point, not a build-specific minimum. The same +API accepts tool descriptors with package identity, detected version, and intent +when the runner has that information. + +If only Git matches, the result may lack access Node needs; a defined result +does not prove both tools matched. A runner needing that distinction uses +diagnostics. Absence or rejection stops request creation. Library failures +propagate to the application error boundary; execution failures use the runner's +normal error handling, never an automatic expansion of access or uncontained +retry. Sharing one container also shares its combined access, not separate +permissions per tool. diff --git a/docs/mxc-policy-store.md b/docs/mxc-policy-store.md new file mode 100644 index 000000000..b1d0a9507 --- /dev/null +++ b/docs/mxc-policy-store.md @@ -0,0 +1,895 @@ +# Feature Spec: Known-tool Policy Floors + +**Status:** Under review. Pending API sign-off from the designated MXC SDK +reviewers before merge and implementation. Not part of MXC 1.0; no later release +is committed. + +**IMPORTANT NOTE:** The Policy Store provides best-effort baseline requirements +believed necessary for representative tool workflows. Floors may broaden file +access to unblock tools, but guarantee neither success nor safety. Users and +clients must review that access; their settings and enterprise policies can +further constrain or override the recommendation. + +--- + +## 1. Problem Statement + +When users first enable sandboxing without known-good configurations, tools +can break and users spend time debugging or disable containment. The catalog +supplies a suggested starting policy intended to make requested tools work +out of the box when the user has not configured the sandbox and no enterprise +policy overrides it. + +Clients can configure the sandbox and override these defaults; user and +enterprise policy can further restrict them, including later changes. The +catalog is a best-effort suggestion, not authorization or a guarantee of +success or safety. + +[#779](https://github.com/microsoft/mxc/pull/779) proposed the initial config +floor data model and SDK resolver. This document develops that proposal into +an SDK catalog contract with identity, platform, dependency, revision, and +inspection behavior. It reuses #779's model where possible and calls out +differences directly. + +For caller-facing review, start with the [API spec](mxc-policy-store-api.md). + +This document does not restate general MXC sandboxing concepts already covered +by the [v1 SDK reference](https://github.com/microsoft/mxc/blob/894f4c159705f5f470727e4fa1e363a2abec88f1/docs/reference/node/v1/README.md) or +[`docs/versioning.md`](versioning.md). It covers only what a policy store adds. + +### Non-goals + +- This does not change backend enforcement or the MXC 1.x request contract. + Catalog data uses the access fields of `ContainerRequest`; it does not + define a parallel policy vocabulary or supply commands. +- This is not a trust or attestation mechanism, and it does not authorize + anything. See [§9](#9-trust-model). +- This is not a guarantee that a complete tool workflow will succeed under + process containment. +- This does not define how any specific consumer stores, displays, or lets a + user approve requirements. See [§2](#2-ownership-boundary). +- This does not define Learning Mode's candidate-generation or review UX. See + [§8](#8-relationship-to-learning-mode). + +### MXC feature impact and defaults + +**MXC 1.0 alignment:** Command-free lookup and composition are unchanged. +`ContainerRequirements` reuses the filesystem, network, UI, and timeout types +from the [v1 request](https://github.com/microsoft/mxc/blob/894f4c159705f5f470727e4fa1e363a2abec88f1/sdk/node/src/v1/types.ts#L549-L597); +the caller adds execution settings later. The SDK owns wire version selection. + +| Earlier spec | MXC 1.x alignment | +|---|---| +| `SandboxPolicy`, `resolveSandboxPolicy` | `ContainerRequirements`, `resolveToolRequirements` | +| `SandboxConfigResolution.policy` | `ToolRequirementsResolution.requirements` | +| `default.sandboxPolicy` and its `version` | Access-only `default.requirements`; catalog `sdkContractVersion` records validation target | +| `ui.allowWindows` | `ui.disable` with inverse meaning; `clipboard` and `allowInputInjection` retain their meanings | +| Synchronous Node resolution | Promise-returning plain verbs, with no `Async` suffix | + +This is an MXC SDK API, not a command-line utility. Following the feature-impact +checklist in +[`docs/authoring-a-new-feature.md`](authoring-a-new-feature.md): + +- **Policy changes:** None. Entries use the existing v1 request access fields. +- **ContainerConfig changes:** None. The catalog does not add configuration + fields or change omission behavior in an existing contract. +- **OS and backend changes:** None. Backends continue to validate whether they + can enforce the resolved policy. +- **MXC SDK changes:** Add policy-resolution and inspection APIs to the existing + TypeScript/JavaScript, Rust, and C#/.NET SDKs, returning `ContainerRequirements`. +- **Delivery:** V1 policy data is embedded in the MXC native library at build + time and ships in the existing MXC SDK packages. See + [§6](#6-intended-repository-and-packaging-boundary). + + +Caller defaults and omission behavior are defined in the +[API spec](mxc-policy-store-api.md#context-defaults-and-symbol-resolution). +Lookup is command-free and opt-in; unresolved requirements never authorize a +less restrictive execution fallback. + +## 2. Ownership boundary + +MXC owns the reviewed, versioned, read-only catalog, its resolution and +inspection APIs, and their SDK publication lifecycle in `microsoft/mxc`. +The requirements type and its underlying request field types remain MXC-owned as well. + +The catalog states a best-effort baseline for a tool. It does not grant +access, modify caller state, create a sandbox, or guarantee workflow success. +Filesystem and network composition combine lower-bound requirements using the +least restrictive access needed by the selected tools. This is distinct from +the consumer's restrictive composition with its own security ceilings. + +Everything else is a consumer decision: + +- Whether automatic catalog lookup is enabled at all. +- Access-profile mapping, elevation preference, and per-tool authorization. +- Persistence of accepted requirements (which tools, which catalog/entry + revision, when). +- Composition with the consumer's own user, learned, and invocation-specific + policy layers, and with non-overridable OS/enterprise/device ceilings. +- Approval UX, audit, and the final call to the v1 `run` / `spawn` operations + (or their language equivalents). + +A catalog lookup can only ever narrow what a consumer still has to decide for +itself. `resolveToolRequirements` yields candidate requirements or +`undefined`; its diagnostics counterpart also reports how that result was +obtained. The consumer decides whether and how to act on it. This mirrors +#779's floor/policy distinction, discussed further in +[§3](#3-relationship-to-the-config-floors-proposal): a resolved entry is a +lower bound asserted by the tool ecosystem, never an upper bound the host is +required to grant. + +## 3. Relationship to the config-floors proposal + +| #779 (config floors) | This document (policy store) | +|---|---| +| One `schemaVersion` for the whole table | Separate catalog shape, catalog/entry revisions, and SDK-owned validation target ([§4.1](#41-versions)) | +| Strongest satisfied identity predicate describes a match | Select the most specific identity/intent/architecture match per tool; tied matches are errors ([§4.3](#43-identity)) | +| One `sandboxPolicy` per entry; `when.platform` only conditions dependencies | One unversioned default per entry, with platform/intent data and optional additive version overlays ([§4.2](#42-entry-shape)) | +| `requires` composition unspecified beyond "union" | Composition limited to a small, explicit, field-by-field set for the first contract version; everything else is rejected until a rule exists ([§4.5](#45-dependencies-and-composition)) | +| `getSandboxConfigForTool(tools: string[])` returns one composed policy | Replaced by `resolveToolRequirements`, accepting one tool or an array and optional lookup context; `resolveToolRequirementsWithDiagnostics` adds attribution ([§5](#5-api-surface)) | +| No revision/publication model | Immutable published catalog revisions; corrections publish a new revision ([§10](#10-immutable-revisions)) | + +The data model, the floor/policy direction argument, multi-tool composition, +and the identity layering problem (invocation name vs. launcher artifact vs. +executing image) build on #779. The matching, composition, and trust refinements +are stated here rather than implied by that reference. + +## 4. Data model + +### 4.1 Versions + +| Field | Meaning | +|---|---| +| `catalogSchemaVersion` | Version of the catalog JSON shape itself. | +| `catalogRevision` | Immutable identifier for one published, fully reviewed catalog. | +| `entryRevision` | Monotonic revision of a single entry, for cache invalidation and audit comparison. | +| `sdkContractVersion` | Catalog-revision metadata recording its published validation target (`1.0.0` initially), not a caller-selected wire version. | + +These identifiers serve separate purposes and do not advance in lockstep. +Registering a new exact request contract does not change existing catalog +data. A semantic entry change increments its `entryRevision` and the containing +`catalogRevision`; changing only catalog metadata, including its published +validation target, increments only `catalogRevision`. Revalidating unchanged +data does not rewrite its published metadata or either revision. +Tool versions and `versionRange` values are distinct from catalog revisions. + +Entries use MXC's v1 request access fields; breaking changes require a new +major surface. No entry or caller chooses a wire version. At SDK build time, +every bundled revision is revalidated against that SDK's exact +`SDK_CONTRACT_VERSION` and its matching schema, not automatically accepted by +semver range. An older published validation target remains selectable when +it is an exact registered stable target in the same major and the current +build has revalidated its data successfully. Other revisions are not bundled. +At lookup, select only those build-validated revisions; never dispatch their +historical target or substitute another revision. The SDK owns wire version +selection as described in [versioning.md](versioning.md). + +### 4.2 Entry shape + +```json +{ + "entryId": "tool:git", + "entryRevision": 3, + "displayName": "Git", + "versionScheme": "intdot", + "identity": [ + { "kind": "purl", "value": "pkg:generic/git" }, + { "kind": "invocation-name", "names": ["git", "git.exe"] } + ], + "default": { + "requirements": { + "filesystem": { + "readonlyPaths": ["${git_prefix}"], + "readwritePaths": ["${project_root}"] + } + }, + "intents": { + "local": { + "exampleSubcommands": ["status", "diff"], + "policyAdditions": {} + }, + "fetch": { + "exampleSubcommands": ["fetch", "pull"], + "policyAdditions": { + "network": { + "egress": { + "allow": [ + { "to": [{ "cidr": "192.0.2.10/32" }], "ports": [{ "protocol": "tcp", "port": 443 }] } + ] + } + } + } + }, + "push": { + "exampleSubcommands": ["push"], + "policyAdditions": { + "network": { + "egress": { + "allow": [ + { "to": [{ "cidr": "192.0.2.10/32" }], "ports": [{ "protocol": "tcp", "port": 22 }] } + ] + } + } + } + } + } + }, + "platformVariants": [ + { + "when": { "platform": "windows" }, + "policyAdditions": { + "filesystem": { "readonlyPaths": ["${programData}/Git"] } + } + } + ], + "versionVariants": [ + { + "versionRange": "vers:intdot/>=2.40|<2.50", + "intentAdditions": { + "push": { "dependencies": [{ "entryId": "tool:ssh" }] } + } + }, + { + "versionRange": "vers:intdot/>=2.50|<3", + "newIntents": { + "bundle-fetch": { + "exampleSubcommands": ["clone --bundle-uri="], + "policyAdditions": { + "filesystem": { "readwritePaths": ["${temp_dir}/git-bundles"] }, + "network": { + "egress": { + "allow": [ + { "to": [{ "cidr": "198.51.100.20/32" }], "ports": [{ "protocol": "tcp", "port": 443 }] } + ] + } + } + } + } + } + } + ], + "provenance": { "method": "reviewed-observation", "sourceRevision": "opaque-review-reference" } +} +``` + +Each entry has exactly one unversioned `default`, containing the conservative +subset common to all tool versions and platforms, not the newest version's +behavior. It contains minimal base access fields in `requirements` and intent +additions. Neither stored nor resolved requirements contain execution settings. +The caller supplies a command later when constructing a `ContainerRequest`. +Unversioned means no tool-version selector, not a caller-selectable wire version. + +`versionVariants` is an optional list of non-overlapping VERS ranges using the +entry's `versionScheme`. Effective policy data is `default` plus the selected +platform/architecture overlay and at most one selected version overlay. +Neither dimension cascades across multiple variants. In either overlay, +`policyAdditions` and `dependencies` add to the base, `intentAdditions` adds to +intents the default declares, and `newIntents` defines intents the default +does not declare. Additions use only fields +with defined monotone composition: no replacements, deletions, narrowing, +negative operations, or removal/renaming of inherited intents. +In v1, access additions are read-only/read-write paths and outbound allow +rules. Overlay deny rules, scalar replacements, and delete/rename operations +are invalid. + +To narrow requirements for newer versions, remove the access from the default +and add it only to the older version ranges that need it. Each effective +variant must retain all default access and intents. Intent bodies themselves +remain additions to their effective base, not full policy copies. + +The Git example illustrates the structure, not a verified Git compatibility +matrix. `local` adds nothing; default fetch and push add their network needs. +The first range adds an SSH dependency to push through `intentAdditions`. The +second defines `bundle-fetch` through `newIntents`, without inheriting the +first range's SSH dependency. The endpoints are +documentation addresses. The Windows overlay adds `${programData}/Git`; +`programData` is the Windows common application-data directory and is resolved +only when that overlay is selected. + +Intent names are exact identifiers scoped to the tool: Git's `local`, `fetch`, +and `push` are supplied as `intent: "local"`, `"fetch"`, or `"push"`. +`exampleSubcommands` contains non-normative hints for callers. The caller maps +command lines to intents; the catalog does not parse or execute command lines. + +Invariants: + +- `entryId` is stable, unique, namespaced, and is the only key `dependencies` + edges may reference. +- `entryRevision` increases on every semantic change to the entry. +- Exactly one `default` is required, including when no version variants exist. + Entries with only versioned variants, multiple defaults, or a variant tagged + as default are invalid. +- Each entry declares a supported `versionScheme`. Version ranges are valid, + non-overlapping, and use that scheme. Overlap is a catalog error, not a + precedence choice. +- Intent names are unique in each materialized default-plus-overlays result. + `intentAdditions` may name only intents the default declares. `newIntents` + cannot reuse a default intent name or a name the other selected overlay + defines. All additions and dependencies are versioned with the containing + entry. +- Variant selection follows the deterministic rules in + [§4.4](#44-platform-variants). No matching platform overlay leaves the common + default unchanged; it does not produce an empty policy or a no-match result. +- Symbols (`${project_root}`, `${npm_cache}`, OS well-known folders) are + resolved by the resolver before a policy is returned; catalog data never + ships a literal, machine-specific path. This is unchanged from #779. +- Embedded request access fields use the v1 SDK contract and the validation + pipeline below; there is no standalone `SandboxPolicy` schema. + +`default.requirements` is closed to the three filesystem path lists, +directional network policy, v1 UI fields, and unsigned 32-bit `timeoutMs`. +Commands, wire versions, backend configuration, runtime proxy values, +lifecycle/cleanup settings, environment data, and unknown fields are rejected, +including inside overlays. `policyAdditions` retains its access-only meaning. +When UI is present, `disable` is required; `clipboard` and `allowInputInjection` +are optional. UI/timeout composition and overlay limits remain those of §4.5. + +Build validation materializes every platform/architecture/version/intent +combination, enforcing the closed field set, selectors, and additive rules. +Bind fixture symbols and a validation-only command, then expose the SDK's exact +[`OneShotRequest` before normalization](https://github.com/microsoft/mxc/blob/894f4c159705f5f470727e4fa1e363a2abec88f1/src/mxc-sdk/src/policy/exact/v1_0.rs#L184). +This hook must be implemented: `prepare_request` returns normalized data. +Validate the exact serialization against the SDK target's schema, initially +[`mxc-config.schema.1.0.0.json`](https://github.com/microsoft/mxc/blob/894f4c159705f5f470727e4fa1e363a2abec88f1/schemas/stable/mxc-config.schema.1.0.0.json), +mapping `allowInputInjection` to wire `injection` and supplying the SDK-owned +wire version. Check CIDR/exclusion containment, protocol/port relationships, +numeric bounds, and unknown fields. Run semantic normalization on a disposable +copy; its restrictive path precedence must not alter the original floors. +The fixture command is never executed or returned. Lookup repeats validation +with real symbols and the object-identity checks in §4.5. Neither stage probes +or launches a backend; enforcement and capability checks still occur at execution. + +Symbol definitions are shared across entries and versioned with the selected +catalog revision. Each definition describes the symbol's permitted sources +and may include a `defaults` map from platform to path template. This extends +the catalog contract, not `ContainerRequest`. Entries continue to reference +symbols rather than repeat defaults. For example, default metadata in the +shared symbol registry can include: + +```json +{ + "symbols": { + "npm_cache": { + "defaults": { + "linux": "${user_home}/.npm", + "macos": "${user_home}/.npm" + } + }, + "programData": { + "source": "host", + "description": "Windows common application-data directory." + } + } +} +``` + +This is a metadata fragment, not a complete symbol definition. Default +templates may reference approved host-known symbols; they cannot contain +commands or executable discovery logic. Shared definitions and defaults are +embedded with the entries and cannot change behind a pinned catalog revision. + +Symbol discovery implementations follow the +[API precedence and failure rules](mxc-policy-store-api.md#context-defaults-and-symbol-resolution). +Tool-specific discovery is library code, never executable catalog data. + +### 4.3 Identity + +Entries declare package-identity predicates and invocation-name fallbacks, +with tool-defined intents and version overlays. The +[API matching contract](mxc-policy-store-api.md#identity-and-match-precedence) +defines equality, precedence, and malformed-input behavior; the +[version/intent contract](mxc-policy-store-api.md#version-intent-and-platform-selection) +defines selection and diagnostic outcomes. Catalog indexing and validation +must use those same rules, not introduce a second comparison policy. + +Validate complete catalog PURLs and reject invalid predicates. Validate VERS +syntax, supported schemes, and non-overlapping ranges during authoring/build. +Neither predicate ordering nor file order may resolve an ambiguous match. + +### 4.4 Platform variants + +Each platform variant's `when` selector contains `platform` (`windows`, +`linux`, or `macos`) and optional `architecture` (`x64` or `arm64`). +It adds `policyAdditions`, `dependencies`, `intentAdditions`, and +`newIntents` to the common default; it never removes, narrows, or replaces +default requirements. Catalog validation rejects duplicate exact selectors +and more than one architecture-neutral variant for a platform. + +The [API selection rules](mxc-policy-store-api.md#version-intent-and-platform-selection) +define precedence, host defaults, and emulation diagnostics. Architecture is +catalog context, not a new `ContainerRequest` field. Variants remain +backend-neutral; execution still validates backend capabilities. + +### 4.5 Dependencies and composition + +The [API composition contract](mxc-policy-store-api.md#dependencies-and-composition) +defines selected dependencies, effective access, conflicts, and failures. +Implement the same rules within entries, across requested tools, and through +dependency closures. Caller restrictions are never inputs to floor composition. + +Dependency references use `entryId` and may name intents, for example +`{ "entryId": "tool:ssh", "intents": ["connect"] }`. Validate that those +intents exist in every materialized platform combination where referenced. +A dependency range is validated VERS metadata, not detected version evidence. +Traverse transitively, rejecting cycles. + +De-duplicate each source layer independently within an entry/catalog revision: +default base once; platform base by selector; default intent by name; platform +intent by selector/name; version base by range; version intent by range/name. +New intents belong to their defining overlay. Do not attach a requesting +pair's version or intent to a shared base key. Preserve all requesting input +indexes for direct and transitive attribution. + +Resolve symbols and validate bound values before floor composition. Lexical +normalization respects platform path syntax and actual case rules; retain +original casing. It does not establish filesystem-object identity. +Use MXC's [object comparison primitives](https://github.com/microsoft/mxc/blob/894f4c159705f5f470727e4fa1e363a2abec88f1/src/mxc-sdk/src/core/mxc_common/filesystem_object.rs#L100-L265): +Windows volume serial/file ID via `CreateFileW`/`FileIdInfo`, Unix device/inode +via `stat`. Resolve existing ancestors for subtree comparison. A cleanly +missing suffix is relative to its deepest established ancestor, not an existing +alias; unreadable components and unresolvable broken links are unknown. + +Preserve required alias locations, including bind mounts, symlinks, junctions, +hard links, and 8.3 names, even when object identity is shared. Reconcile access +without deleting a needed pathname. For unknown relationships, exclude every +affected pair, not just the restrictive side; remove solely owned layers and +retain shared layers for remaining contributors. Recompose complete surviving +pairs. This resolver observation does not replace enforcement-time checks. + +Catalog validation exercises composition with fixtures, but caller symbols and +filesystem observations require lookup-time validation too. Materialization +must not invoke the runner's restrictive normalization as the floor merge +algorithm. Unsupported composition fails explicitly. + +## 5. API surface + +The [Policy Store API spec](mxc-policy-store-api.md) is the authoritative +caller-facing contract: public types, signatures, defaults, behavior, errors, +diagnostics, and usage examples. This document owns catalog authoring and +implementation. API review comes first; implementation follows the approved +contract. + +### 5.1 Runtime lookup + +`resolveToolRequirements` resolves one tool or an array into command-free +`ContainerRequirements`; `resolveToolRequirementsWithDiagnostics` also +reports coverage, selections, dependencies, and warnings. +See [operations](mxc-policy-store-api.md#1-operations), +[types](mxc-policy-store-api.md#2-types-and-fields), and +[results/coverage](mxc-policy-store-api.md#results-coverage-and-attribution). + +### 5.2 Setup and inspection + +`listCatalogEntries` and `getCatalogInfo` expose metadata without resolving +policy bodies. Their signatures and +[metadata types](mxc-policy-store-api.md#inspection-metadata) live in the API spec. + +### 5.3 Consumer obligations + +Consumers own authorization, their policy layers, accepted-policy persistence, +and execution. The [API responsibilities](mxc-policy-store-api.md#4-errors-and-consumer-responsibilities) +define the contract; the [trust model](#9-trust-model) explains its rationale. +Catalog APIs never write a consumer's store or mutate reviewed data at lookup. + +## 6. Intended repository and packaging boundary + +The Policy Store lives in `microsoft/mxc` and ships through the existing MXC +SDK packages, not a separate repository, package, or command-line utility. +Catalog data and resolver APIs follow the MXC contribution and release process. + +One internal `policy_store` module in `mxc-sdk` uses the SDK's v1 types and +common identity helpers. Rust calls it directly; Node/.NET use panic-contained +`mxc_ffi` library exports, not launch/probe APIs. V1 data is embedded at build +time, with no dynamic fetching; that is a possible V2 capability. + + +### 6.1 Library distribution and consumption + +Resolution and inspection ship through the existing TypeScript/JavaScript, +Rust, and .NET SDKs. The [API spec](mxc-policy-store-api.md#1-operations) owns +entry-point names, return types, and language conventions. SDK reference pages +and package READMEs should link that contract rather than duplicate it. + +V1 data updates ship with MXC releases; catalog revision metadata does not +imply an independent download channel. A package update never rewrites a +consumer's previously accepted requirements. + +### 6.2 Cross-language consistency and support + +All three SDKs use the same catalog format and shared conformance +fixtures. Given the same catalog revision, tool inputs, explicit resolution +context, and relevant host/filesystem observations, they must agree on matching, +variant selection, dependency metadata, effective policy, diagnostic meaning, +and failure categories. This is semantic consistency, not byte-identical +output or reproduction of another language's runtime behavior. + +Each binding uses consistent, idiomatic result and error handling for its +language. Equivalent failures map to the corresponding existing MXC error +codes, while error types, message wording, and language-specific representations +may differ. Callers need not understand another binding or parse message text. +Shared fixtures compare policy semantics and required diagnostic information, +including prescribed ordering, rather than identical warning prose or +incidental serialization. + +Shared fixtures cover platform path semantics as well as ordinary lookup; +matching function names alone is not compatibility. Package CI must also +exercise installation, public API usage, and host-derived defaults on the +supported platforms. Binding tests exercise the shared native resolver rather +than independent implementations of the resolution rules. + +Policy Store is an ongoing SDK capability. Language parity, +documentation, and maintenance belong to the MXC SDK release process. + +### 6.3 Catalog source layout and embedded data + +Catalog authors edit one JSON file per tool under +`src/mxc-sdk/policy_store/catalog/entries/`. Each file contains that tool's full +entry: identity, common default, intents, all platform/version variants, +dependencies, and provenance. Do not split variants or intents into separate files. + +```text +src/mxc-sdk/policy_store/ + catalog/ + contract.v1.json + manifest.json + entries/ + git.json + node.json + npm.json + revisions/ + .json # generated release snapshot + views/ + .md # generated reviewer view + .requests.json # generated validation requests + schema/ + catalog.v1.schema.json + manifest.v1.schema.json + conformance/ +``` + +Build tooling collects entry files recursively, validates globally unique +`entryId` values and dependency references, and assembles the complete candidate +revision in stable entry-ID order. CI checks generated snapshots/views against +their editable inputs; authors do not maintain a second policy copy by hand. +Optional category subdirectories are organizational only: paths and file order +never affect identity, lookup, or composition. + +`manifest.json` selects the default revision and maps bundled revisions to +generated snapshots. Published snapshots remain immutable; changing tool data +produces a new catalog revision and the required entry revision changes. +Generating a new revision never overwrites a published one. Moving an entry +between category directories alone is not a semantic policy change. + +The build embeds validated snapshots into the native library. These are source +and generated-artifact paths, not runtime lookup directories or files consumers +edit. Consumer persistence remains separate from the read-only MXC catalog. + +## 7. Contribution and review + +- Catalog and API contributions are pull requests in `microsoft/mxc`. + No client or SDK can write a catalog entry at runtime. +- Every entry change includes identity evidence, supported tool version + range(s), platform evidence, a minimized requirement set, test fixtures, + and provenance. +- Entry and dependency `versionRange` values pass VERS syntax and supported-type + validation during authoring/build validation. An entry's version ranges must + use its declared scheme and must not overlap. +- Keep one unversioned default common to all versions and platforms. Define an + intent only when its additions materially differ in network access, + credentials, or writes outside the + workspace. Do not create one intent per subcommand. An empty `local` intent + identifies base-only use separately from omitted-intent aggregation. +- Intent dependencies and access additions must be justified by that intent. + Example subcommands are caller guidance, not executable matching rules. +- CI must validate the agreed policy contract and version mapping, + entry-ID uniqueness, dependency closure and cycle-freedom, symbol validity, + absence of unsafe user-specific literal paths, unsupported-field rejection, + deterministic resolution, and package inclusion. +- Build validation rejects missing/multiple defaults, version-only entries, + variants tagged as default, and non-additive platform/version operations. + Materialize and schema-validate every platform x architecture x version + variant x intent combination, including omitted-intent aggregation. +- Verify default base requirements are a subset of each effective base, and + each inherited default intent's policy/dependencies are a subset of the same + effective intent. Overlays may add names through `newIntents`, never delete, + rename, or redefine inherited intents. Validate dependency closures + introduced by additions too. +- Generate rendered effective-policy views and diffs from the default for + catalog reviewers; do not require reviewers to mentally expand overlays. +- A new entry or a requirement expansion requires one catalog-owner approval + and one security/policy-reviewer approval, plus tool- or scenario-owner + evidence where available. +- A requirement reduction needs regression evidence for the representative + scenarios used to justify the entry, not a universal workflow guarantee. +- Catalog data and SDK changes follow MXC's contribution and release review. + These requirements do not require applications to seek maintainer approval + to use the SDK APIs. + +## 8. Relationship to Learning Mode + +Learning Mode complements Policy Store; it does not replace it. Policy Store +is a long-lived source of best-effort baseline requirements. +Learning Mode is developer tooling for discovering requirements and +productizing them as reviewed policy data. + +MXC's learning-mode capabilities (`learningModeLogging`, +`permissiveLearningMode`, `captureDenials`; see +[`docs/learning-mode/capabilities.md`](learning-mode/capabilities.md)) are the +substrate a contributor can use to observe what a tool actually touches, the +same way [#779 §5.1](https://github.com/microsoft/mxc/pull/779) describes for +config floors. That observation workflow is unchanged by this document and +remains **a contributor step that happens before a pull request**, not +consumer runtime behavior and not a catalog-mutation path. + +Whether and how a consumer turns its own runtime capability observations into +a candidate catalog contribution or a locally scoped policy suggestion is +that consumer's design. The SDK provides no runtime submission hook or +automatic mutation of the release-bundled catalog. + +## 9. Trust model + +This document tightens #779's trust framing rather than replacing it. Entries +assert *need*, not authorization, but incorrect data has two different +outcomes: + +- An understated floor omits a requirement and can cause the tool or its + end-to-end workflow to fail under the resulting policy. +- An overstated floor can fail against a narrower consumer ceiling. If a + consumer instead approves or adopts it and its ceiling permits the request, + the effective policy contains unnecessary capability. + +Even correct entries can yield a broader combined request than any one tool +needs. Read-write requirements supersede overlapping catalog read-only +requirements, and a conflicting catalog filesystem or egress deny is removed +in full under +[§4.5](#45-dependencies-and-composition). These changes are intentional and +reported by the diagnostics API; they are not permission to remove a +consumer's own restrictions. + +What changes from #779 is the review bar. #779 described community-contributed, +unsigned, unwarranted data. This contract requires named-role approval +([§7](#7-contribution-and-review)) before an entry publishes, and publishes +under an immutable revision through MXC's package distribution +([§10](#10-immutable-revisions)). +That raises confidence in the data; it does not change what the data *is*. The +catalog still carries no security guarantee or independent authority. A +consumer must review the requirement and intersect it with its own policy +rather than adopt it outright. The catalog can influence a consumer's +decision, so consumer approval and restrictive ceilings remain required even +though the catalog cannot grant capability by itself. See +[#779 §2.1](https://github.com/microsoft/mxc/pull/779) for why that layering is +honest about what such a choice costs. + +## 10. Immutable revisions + +Published catalog revisions are immutable. A correction, including a security +fix to an over-broad entry, publishes a new `catalogRevision` and bumps the +affected `entryRevision`; it never rewrites a revision a consumer may already +have cached or recorded in an audit trail. + +V1 embeds entries and shared symbol definitions in the native library at build +time and inherits MXC package signing and distribution integrity. It has no +separate catalog digest or runtime checksum. Schema validation still applies. + +## 11. Backward compatibility + +- No change to the v1 `ContainerRequest` or exact `ContainerConfig` schema. +- No change to executor behavior. +- New opt-in APIs in existing MXC SDKs; callers that do not use policy lookup + see no behavior change from this feature. +- No breaking policy-schema changes within MXC 1.x; breaking changes require + 2.x. Bundled entries follow the containing SDK's compatible policy contract. + +## 12. Test plan + +**SDK resolver APIs (TypeScript/JavaScript, Rust, and C#/.NET)** + +- shared conformance fixtures produce equivalent results and failure + categories in all three languages without requiring identical message text + or language-specific representations; each binding's error handling is + consistent and uses the corresponding MXC error codes +- failure cases use the [API error mappings](mxc-policy-store-api.md#4-errors-and-consumer-responsibilities); + any supplied reason uses its listed value, and callers can handle the code alone +- one-tool and one-element-array overloads produce equivalent requirements and + diagnostics; the simple API yields the same requirements as the diagnostic API +- a separate TypeScript consumer imports every public function and named type + from the v1 package entry point; same-file snippet checks are not sufficient +- lookup is command-free and returns only §4.2's allowed fields; reject old + UI names and caller execution fields rather than silently accepting them +- Node resolution does not block the event loop; Rust/.NET preserve their + corresponding idiomatic result/error types +- multiple input tools select one match each and compose their bases, selected + intent additions, and dependencies; repeated inputs and shared components + do not duplicate contributions +- two versions or intents of one entry share the default/platform base layers + once, preserving distinct additions and per-input attribution; identical + base fields do not create false unsupported-composition conflicts +- a known intent selects only its effective base and additions; omitted intent + combines all effective intents, while unsupported intent contributes nothing + with `intent_unsupported` +- omitted version selects default with `matched_default` and no version warning; + a version in one range adds only that overlay with `matched_version` +- a valid version outside all ranges uses default with `version_out_of_range`; + never select the nearest, highest, or broadest variant +- an unparseable version contributes nothing with `version_unparseable`; + an out-of-range version plus a newer-only intent emits both warnings +- unparseable versions, unsupported intents, and unmatched tools skip that + pair's dependencies and symbols while other pairs still resolve +- every input has an ordered status record; `tool_unmatched` never falls back + to a wildcard entry; mixed requests return only contributing policies +- requirements-only and diagnostic APIs retain the same partial-result behavior; + only the diagnostic API reports coverage, without a `requireAllMatches` option +- different tools in one request carry independent intents; two intents of + the same tool share the base without losing either set of additions +- unselected intent dependencies and symbols are not resolved; selected + intent dependencies participate in cycle detection and attribution +- shared and transitive dependency records retain the union of requesting + `inputIndexes`, including successful resolutions that emit no warnings +- a dependency contributes only its default and applicable platform base + additions, never a version overlay or intents, unless its reference names + dependency intents; named intents are added with their platform additions +- known and unknown inputs compose the known requirements and report each + unmatched input; empty and all-unmatched arrays return no policy, never an + empty policy, while the diagnostic API preserves the resolution metadata +- omitted optional context fields use host platform and native system architecture, the + installed catalog revision, no caller symbol overrides, and no weak-identity + fallback +- an unresolved required symbol prevents policy output, with diagnostics, + rather than silently omitting selected requirements +- caller symbol overrides precede discovery, which precedes documented + defaults; only required symbols are resolved, with source/value diagnostics +- failed configuration reads are errors, not default selection; another + target environment does not inherit this host's discovered paths +- pinned catalog revisions retain their shared symbol definitions and defaults; + unsupported default templates and executable discovery data are rejected +- package identity precedes invocation name, then declared intent specificity, + then exact architecture over platform-only or common default; + distinct matches tied at the highest rank produce `ambiguous_match` +- intent declaration may identify an entry before version selection, but a + version lacking that intent still contributes nothing, not another entry +- multiple predicates matching the same entry contribute that policy once; + file order does not change matching or composition +- string shorthand and object inputs obey the same weak-identity fallback + option; intent selection does not bypass it +- invocation names compare case-insensitively on Windows/macOS and exactly on + Linux, without changing command spelling or package-identity matching +- PURL type compares case-insensitively; namespace and name follow type-specific + rules, independent of host OS; case-distinct Maven coordinates remain distinct + and equivalent percent-encodings compare after parsing +- candidate PURL version/qualifiers/subpath are ignored with structured + warnings; malformed components leave only that pair unmatched, with no + invocation-name retry even when weak matching is enabled +- filesystem equality, de-duplication, and ancestor checks follow the actual + case rules, including case-sensitive macOS volumes and Windows directories; + unknown sensitivity preserves differently cased paths with a diagnostic +- exact-architecture additions precede platform-only additions; duplicate + selectors are rejected; no matching overlay retains the common default +- on an ARM64 host with both architecture-specific variants and no neutral + variant, omitted architecture selects ARM64; explicit x64 selects x64 +- a library process running as x64 under emulation on an ARM64 host still + defaults to the native ARM64 system architecture, not its process + architecture +- a missing exact overlay falls back to the platform's neutral additions, + otherwise to the common default; never use another architecture's overlay +- successful host-derived selection and neutral fallback produce the + [API diagnostics](mxc-policy-store-api.md#warnings); host-architecture + detection failure produces a library error, not a guessed match +- dependency chain resolution, including cycles (terminate, no duplication) +- filesystem floor composition ([§4.5](#45-dependencies-and-composition)): + same-class de-duplication; equal read-only/read-write paths become read-write; + read-write ancestors subsume read-only descendants, while read-only + ancestors remain read-only outside required writable subtrees +- literal paths and distinct symbols that resolve to equal or nested paths + follow the same lookup-time composition rules; catalog publication checks + do not substitute for this runtime pass +- symlink, junction, hard-link, bind-mount, and 8.3 aliases use established + target object identity in floor composition; the returned policy must not + silently lose required writes to a more restrictive catalog alias +- same-object aliases preserve every required pathname; a read-write `/data` + and read-only bind-mount alias `/alias` remain accessible through both names + with the composed access, and same-class aliases are not collapsed +- unestablished necessary target identity fails closed for affected pairs, + including dependency aliases, with structured diagnostics; unrelated + resolved pairs still contribute and MXC enforcement is not weakened +- warnings for architecture, symbols, composition, removed denies, versions, + intents, identity, and unmatched pairs have discriminated codes and required + data fields; consumers read paths/rules and source entries without parsing + messages +- catalog denies equal to, above, or below required read/write paths are + removed; non-overlapping denies remain; warnings identify source entries, + paths, access changes, and the full scope of removed parent denies +- overlaps within one entry, across matched inputs, and through dependencies + behave identically; both APIs return equivalent composed policies, and + entry/input traversal order does not change effective access +- a no-network tool does not veto another selected tool's network requirement; + unselected version/platform/intent additions add no access; omitted intent + selects all effective intents, while unsupported intent contributes nothing +- composed outbound rules retain destination/port pairings and exclusions; + no network requirement means no grants, not unrestricted access +- a catalog egress deny rule overlapping another selected tool/intent's + required allow rule is removed in full, within one entry or across entries, + without failing the request; non-overlapping deny rules remain; warnings + identify the rule, its full scope, and source entries +- incompatible SDK target metadata and network fields other than egress allow/deny rules, + or other fields outside the supported composition rules, remain rejected + rather than broadly approximated +- symbol resolution on Windows, Linux, and macOS + +**Data (CI)** + +- every default and materialized platform/architecture/version/intent + combination passes the closed catalog checks, typed v1 builder, semantic + validation, and exact schema validation; no backend/probe is needed +- per-tool sources assemble deterministically across directories; duplicate + entry IDs, dangling dependencies, and generated-source drift fail validation +- validation captures the exact request before normalization and never feeds + the normalized restrictive copy back into floor composition +- a newer SDK exact target revalidates all bundled catalog revisions using + its own schema; unchanged older-major-line revisions retain their IDs when + revalidation passes, while unvalidated or cross-major data is not bundled +- catalog PURLs pass complete syntax/type validation before identity indexing +- exactly one unversioned default is required; missing/multiple defaults, + version-only entries, and a version variant tagged as default are rejected +- version ranges use the entry's scheme and do not overlap, including at + inclusive boundaries; adjacent non-overlapping ranges are accepted +- platform, version, and intent additions share the SDK target and cannot + remove, narrow, or replace inherited requirements; defaults remain subsets + of effective variants and inherited intent names are never deleted or renamed +- rendered effective policies and default-relative diffs include base/intent + access and dependencies for reviewer inspection +- malformed VERS syntax, invalid constraints, and unsupported version types + fail catalog validation; valid examples cover npm, semver, pypi, nuget, and + intdot, including their scheme-specific version syntax +- metadata distinguishes default, platform, and version additions and lists + intent names, subcommand hints, and dependencies without resolving policy bodies +- `dependencies[].entryId` references resolve within the same catalog revision; + named dependency intents exist in every applicable platform combination +- no literal absolute user-specific paths; no wildcard filesystem/network grants +- catalog/entry revision monotonicity across a change + +**Integration** + +- each MXC SDK resolves through the shared Rust implementation with build-time + embedded data; lookup performs no dynamic fetching or sandbox execution +- compiled consumer examples in all three languages pass both the direct + result's fields and the diagnostic result's requirements fields into + `ContainerRequest` with a caller-supplied command, without nested-type + conversion, casts, or serialization +- the bundled catalog revision matches `getCatalogInfo()`; selecting an + unavailable revision fails explicitly, without falling back to another + revision +- an MXC SDK update leaves previously accepted consumer policies unchanged +- a representative tool that fails under a minimal consumer policy succeeds + once its resolved entry is composed in +- the composed MXC policy realizes the documented read-only/read-write + nesting without unnecessarily promoting a read-only parent to read-write; + retained backend precedence does not reintroduce removed catalog conflicts +- the same tool still fails when the consumer's policy forbids what the entry + requests (the floor never widens the consumer's ceiling) +- a caller-owned filesystem or egress deny still prevents access even when an + overlapping catalog-provided deny was removed during floor composition +- floor alias reconciliation and MXC's final object-based normalization agree + on required access; a resolver check never replaces enforcement-time checks + +## 13. Open questions + +| Maintainer sign-off | Recommended answer | +|---|---| +| Approve the command-free requirements API surface? | Review the [API spec](mxc-policy-store-api.md) before implementation, including inputs, results, errors, and partial-result behavior. | + +## 14. Related work + +- [`microsoft/mxc#779`](https://github.com/microsoft/mxc/pull/779) - Sandbox + Config Floors feature spec. This document's data model, floor/policy + direction argument, and identity-layering analysis build directly on it. +- [`ChazGo/mxc#1`](https://github.com/ChazGo/mxc/pull/1) - draft SDK resolver + and catalog prototype exercising lookup, dependency closure, and symbol + resolution against an earlier version of this shape. +- [Node v1 types](https://github.com/microsoft/mxc/blob/894f4c159705f5f470727e4fa1e363a2abec88f1/docs/reference/node/v1/types.md) - + `ContainerRequest` and its access sections. Rust and .NET use their + corresponding v1 SDK types. +- [`docs/versioning.md`](versioning.md) - the versioning model + [§4.1](#41-versions) builds on. +- [Package-URL VERS specification](https://github.com/package-url/vers-spec) - + version-range syntax and supported version-type comparison references.