From a0f55c9e8d5860355be751274bc43b2d327c93cc Mon Sep 17 00:00:00 2001 From: Chaz Gordish Date: Mon, 28 Sep 2026 09:23:26 -0700 Subject: [PATCH 01/12] docs: add MXC Policy Store feature spec Proposed feature spec for the MXC Policy Store: an integrity-validated, versioned, read-only known-tool sandbox requirements catalog and SDK resolver. Builds on microsoft/mxc#779's config-floor data model and tightens it into a review-ready contract: independent catalog/entry/ policy versioning, ordered strong/weak tool identity, complete per-platform requirement variants, deterministic cycle-rejecting dependency resolution with an explicit v1 composition vocabulary, a resolver API split from catalog metadata inspection, immutable published revisions, and a reviewed PR/CI contribution pipeline. Scoped to what MXC owns; consumers retain access-profile mapping, authorization/elevation, persistence, composition, approval, audit, and final sandbox creation. Status is proposed and review-ready, not approved or shipped; open questions are called out explicitly with recommended answers. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 1 + docs/mxc-policy-store.md | 415 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 416 insertions(+) create mode 100644 docs/mxc-policy-store.md diff --git a/README.md b/README.md index 856bf529d..2b6fa7a9c 100644 --- a/README.md +++ b/README.md @@ -316,6 +316,7 @@ 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.md](docs/mxc-policy-store.md) | MXC Policy Store feature spec (proposed known-tool requirements catalog and resolver) | | [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.md b/docs/mxc-policy-store.md new file mode 100644 index 000000000..960d5673a --- /dev/null +++ b/docs/mxc-policy-store.md @@ -0,0 +1,415 @@ +# Feature Spec: MXC Policy Store + +**Status:** Proposed — review-ready draft, targeting sign-off around October 2, +2026. This is a proposed contract for review, not an approved, shipped, or +implemented catalog, and October 2 is a review-readiness target rather than a +delivery or personal commitment. + +--- + +## 1. Problem Statement + +[#779](https://github.com/microsoft/mxc/pull/779) proposed **config floors**: a +repository-hosted table of minimum sandbox requirements per known tool, plus an +SDK resolver, so a host does not have to discover by hand what a tool needs to +run inside a sandbox. That proposal intentionally left several things loose — +one version number for the whole table, name-only identity as the common case, +one policy per entry regardless of platform, and no distinction between "look up +one tool's requirement" and "inspect the whole catalog." + +Turning that proposal into something MXC can host and consumers can build +against requires tightening exactly those points into a contract: independently +versioned catalog and entries, ordered identity strength, complete per-platform +requirement statements, deterministic dependency resolution, a resolver API +that is safe to call in a hot path, immutable published revisions, and a +reviewed contribution pipeline. This document is that contract. It reuses +[#779](https://github.com/microsoft/mxc/pull/779)'s data model and API shape +almost entirely; where it diverges, it says so. + +This document does not restate general MXC sandboxing concepts already covered +by [`docs/sandbox-policy/0.8.0/policy.md`](sandbox-policy/0.8.0/policy.md) or +[`docs/versioning.md`](versioning.md). It covers only what a policy store adds. + +### Non-goals + +- This does not change what the sandbox backend enforces, or `SandboxPolicy` / + `ContainerConfig` schema semantics. A catalog entry embeds an existing + `SandboxPolicy`; it does not define a parallel vocabulary. +- This is not a trust or attestation mechanism, and it does not authorize + anything. See [§9](#9-trust-model). +- 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). + +## 2. Ownership boundary + +MXC owns exactly one thing here: an integrity-validated, versioned, read-only +catalog of known-tool sandbox requirements, and the SDK surface that resolves +it. The catalog states what a tool needs. It does not grant access, does not +modify caller state, and does not create a sandbox. + +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 tool, 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 into `createConfigFromPolicy()` / + sandbox creation. + +A catalog lookup can only ever narrow what a consumer still has to decide for +itself: MXC returns a candidate requirement or `undefined`; 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 | Four independent versions: `catalogSchemaVersion`, `catalogRevision`, per-entry `entryRevision`, and per-variant `sandboxPolicy.version` ([§4.1](#41-versions)) | +| `identity` predicates, unordered | `identity` explicitly ordered strongest → weakest, with defined match/fallback behavior ([§4.3](#43-identity)) | +| One `sandboxPolicy` per entry; `when.platform` only conditions dependencies | One complete `SandboxPolicy` per platform variant; a variant cannot name a containment backend ([§4.4](#44-platform-variants)) | +| `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)) | +| Single resolver function, no separate catalog-inspection API | Resolver split from a separate metadata/inspection API ([§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, the identity layering +problem (invocation name vs. launcher artifact vs. executing image), and the +trust framing all carry forward from #779 essentially unchanged; this document +does not re-derive them, and cites the relevant #779 section instead. + +## 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. | +| `sandboxPolicy.version` | The exact registered `SandboxPolicy` contract version used by one platform variant. | + +These move independently. A `SandboxPolicy` version bump does not require a new +catalog revision, and a catalog revision does not require every entry to bump. +Tool version constraints (`versionRange`, below) are a fifth, orthogonal axis — +they describe which builds of the *tool* an entry was observed against, not +anything about the catalog. + +### 4.2 Entry shape + +```json +{ + "entryId": "tool:npm", + "entryRevision": 3, + "displayName": "npm / npx", + "identity": [ + { "kind": "purl", "value": "pkg:npm/npm", "versionRange": ">=10 <12" }, + { "kind": "invocation-name", "names": ["npm", "npm.cmd", "npx", "npx.cmd"] } + ], + "platformVariants": [ + { + "when": { "platform": "windows" }, + "dependencies": [{ "entryId": "tool:node", "versionRange": ">=22" }], + "sandboxPolicy": { + "version": "0.9.0-alpha", + "filesystem": { + "readonlyPaths": ["${npm_prefix}"], + "readwritePaths": ["${project_root}", "${npm_cache}"] + } + } + } + ], + "provenance": { "method": "reviewed-observation", "sourceRevision": "opaque-review-reference" } +} +``` + +Invariants: + +- `entryId` is stable, unique, namespaced, and is the only key `requires`/ + dependency edges may reference. +- `entryRevision` increases on every semantic change to the entry. +- Exactly one most-specific `platformVariants` entry may match a given + platform/architecture. No matching variant means the tool is unsupported on + that platform — not that it needs an empty policy, and not `undefined` + conflated with "requires nothing" (see [#779, "Defaults and + omission"](https://github.com/microsoft/mxc/pull/779)). +- 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. +- An embedded `sandboxPolicy` is validated against the real `SandboxPolicy` + schema for its declared `version` — the catalog schema does not duplicate + that validation. + +### 4.3 Identity + +`identity` is an ordered list, strongest predicate first, per the layering +#779 §3.1 establishes (invocation name vs. launcher artifact vs. executing +image; falsifiable-against-a-local-artifact as the admission test for a new +kind). This document adds: + +- A version range on an identity predicate is advisory matching evidence, not + a gate. A detected mismatch returns a diagnostic alongside the resolved + policy rather than silently degrading precision, and the consumer decides + what to do with the mismatch. +- Invocation-name-only identity is the always-available fallback, not the + default outcome. Whether a consumer accepts an invocation-name-only match + automatically, or requires opt-in, is unresolved — see [§13](#13-open-questions). + +### 4.4 Platform variants + +Supported platforms are `windows`, `linux`, and `macos`. A platform variant is +a complete requirement statement — one full `SandboxPolicy`, not a patch +applied to a base policy — plus any platform-specific dependencies. Variants +are never merged. This closes #779's open question about entries that need a +genuinely different policy per platform, not just different symbol resolution: +they now can, by declaring more than one variant. + +A platform variant must not name a specific MXC containment backend. Policies +stay backend-neutral; the selected backend still decides whether a stated +requirement can be realized on that host. + +### 4.5 Dependencies and composition + +Dependencies reference another entry's `entryId`, with an optional tool +`versionRange`, and live inside the platform variant when platform-specific. +Resolution is transitive, cycle-rejecting, and deterministic, and the resolver +returns the full dependency chain alongside the result. + +Unlike #779, this document does not treat "union the policies" as sufficient +composition. Silently unioning arbitrary `SandboxPolicy` objects across a +dependency chain hides exactly the kind of conflicting-field problem that +made #779 exclude `proxy` from the embedded object. For the first contract +version, composition across a dependency chain is limited to fields where the +merge rule is unambiguous: + +- filesystem path lists, normalized and de-duplicated; +- network host lists, normalized and de-duplicated; +- boolean capability requirements, where `true` always means "this dependency + requires the capability." + +Timeout, clipboard, lifecycle, UI, and proxy fields are excluded from +cross-entry composition until each has an explicit rule; catalog validation +rejects a dependency combination that would require merging one of them, +rather than picking an implicit answer. + +## 5. API surface + +Two APIs, kept deliberately separate so that "resolve one tool's requirement" +stays a cheap, hot-path-safe call and never implicitly returns the whole +catalog. + +### 5.1 Runtime lookup + +```ts +interface ToolCandidate { + invocationName: string; + packageUrl?: string; + detectedVersion?: string; +} + +interface ResolveContext { + projectRoot?: string; + symbols?: Record; + platform?: "windows" | "linux" | "macos"; + architecture?: string; + catalogRevision?: string; + allowWeakIdentityFallback?: boolean; +} + +interface ResolvedToolEntry { + entryId: string; + entryRevision: number; + catalogRevision: string; + matchedIdentity: { kind: string; strength: "strong" | "weak" }; + resolvedDependencies: Array<{ entryId: string; entryRevision: number }>; + policy: SandboxPolicy; + warnings: string[]; +} + +resolveCatalogEntry( + tool: ToolCandidate, + ctx?: ResolveContext +): ResolvedToolEntry | undefined; +``` + +`resolveCatalogEntry` is singular by design, not an array-in/array-out call: +a caller composing and persisting requirements per tool (rather than as one +opaque merged blob) needs each result independently addressable and +independently attributable. A caller resolving several tools calls it once +per tool. `undefined` means no acceptable identity/platform match — never an +empty policy (same distinction #779 makes; see [§4.2](#42-entry-shape)). + +### 5.2 Setup and inspection + +```ts +listCatalogEntries(): CatalogEntryMetadata[]; +getCatalogInfo(): { catalogSchemaVersion: string; catalogRevision: string }; +``` + +This supports setup UI, catalog browsing, and update decisions without paying +the cost of policy resolution, and keeps "give me everything" out of the +runtime lookup path entirely. + +### 5.3 Consumer obligations + +A consumer that uses this API: + +1. Decides whether automatic lookup is enabled at all. +2. Stores any accepted result per tool, keyed by `entryId`, `entryRevision`, + and `catalogRevision` — never as an unattributed merged policy blob. +3. Keeps catalog-derived requirements in a layer separate from its own user, + learned, and invocation-specific policy. +4. Applies its own authorization, elevation, and restrictive-composition + rules on top. +5. Enforces its OS, enterprise, device, and backend ceilings regardless of + what the catalog returned. +6. Fails closed — falls back to its own restrictive baseline, and does not + run uncontained — when a required entry cannot be realized on the current + host/backend. +7. Records matched identity, catalog/entry revision, warnings, and approval + state in its own audit trail. + +MXC never writes a consumer's policy store. A consumer's own capability +observation (see [§8](#8-relationship-to-learning-mode)) can produce candidate +evidence for a future contribution to this catalog; it is not a mechanism for +mutating the catalog at request time. + +## 6. Packaging and repository ownership + +Canonical source lives in `microsoft/mxc`, with schema, semantic validation, +and generated package artifacts, following this repo's existing schema +codegen model ([`docs/schema-codegen.md`](schema-codegen.md)). Whether the +catalog ships inside each SDK package or as a separately versioned artifact +consumed by all SDKs is open; a separately versioned artifact is recommended +so catalog updates are not coupled to SDK release cadence. See +[§13](#13-open-questions). + +## 7. Contribution and review + +- Contributions are pull requests against `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. +- CI validates schema conformance, exact `SandboxPolicy` version registration, + identity uniqueness, dependency closure and cycle-freedom, symbol validity, + absence of unsafe user-specific literal paths, unsupported-field rejection, + deterministic resolution, and package inclusion. +- A new entry or a requirement expansion requires one MXC SDK/catalog-owner + approval and one MXC security/policy-reviewer approval, plus tool- or + scenario-owner evidence where available. +- A requirement reduction requires regression evidence that every supported + tool version still functions under the narrower requirement. + +## 8. Relationship to Learning Mode + +MXC's upstream 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 into a locally-scoped policy suggestion +for its own user, is that consumer's design — most likely deferred to the +consumer, and out of scope for the catalog contract itself unless a future +revision of this contract needs to define hooks for submitting observation +evidence. No such hook is proposed here. + +## 9. Trust model + +This document tightens #779's trust framing rather than replacing it: entries +still assert *need*, not authorization, and a wrong or malicious entry can +only overstate need, which surfaces as a tool that fails under the consumer's +existing policy — never as an authority the consumer did not already grant. + +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, integrity-validated revision ([§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, and a consumer must still +intersect a resolved entry with its own policy rather than adopt it as policy +outright (though nothing prevents that choice — 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. + +## 11. Backward compatibility + +- No change to `SandboxPolicy` or `ContainerConfig` schema. +- No change to executor behavior. +- New SDK surface only; existing callers that never call it see no behavior + change. +- Given the schema is expected to move as open questions resolve, the catalog + and resolver should land under the experimental surface and promote through + this repo's normal promotion process once the shape has settled, per + [`docs/authoring-a-new-feature.md`](authoring-a-new-feature.md). + +## 12. Test plan + +**Resolver (SDK unit tests)** + +- single tool → expected entry; unknown tool → `undefined`, never an empty policy +- identity match strength selection and weak-identity fallback behavior +- version-range mismatch produces a warning, not a refusal +- exactly one platform variant selected; no matching variant → `undefined` +- dependency chain resolution, including cycles (terminate, no duplication) +- restricted composition rules ([§4.5](#45-dependencies-and-composition)): + path/host de-duplication and boolean-OR merge; a dependency requiring an + unsupported composed field is rejected at validation time, not resolved +- symbol resolution on Windows, Linux, and macOS + +**Data (CI)** + +- every entry and platform variant validates against the catalog schema and + the `SandboxPolicy` schema for its declared version +- `dependencies[].entryId` references resolve within the same catalog revision +- no literal absolute user-specific paths; no wildcard filesystem/network grants +- catalog/entry revision monotonicity across a proposed change + +**Integration** + +- a representative tool that fails under a minimal consumer policy succeeds + once its resolved entry is composed in +- the same tool still fails when the consumer's policy forbids what the entry + requests (the floor never widens the consumer's ceiling) + +## 13. Open questions + +Recommended answers are proposals for review, not decisions. + +| Question | Recommended answer | +|---|---| +| Repository and package ownership: does the catalog ship inside each SDK package, or as a separately versioned artifact? | Canonical source in `microsoft/mxc`; prefer a separately versioned generated artifact to decouple catalog updates from SDK releases. | +| Is invocation-name-only identity accepted automatically, or does it require explicit consumer opt-in? | Treat it as a fallback requiring explicit opt-in (`allowWeakIdentityFallback`), not the default. | +| What happens on a detected tool-version mismatch — `undefined`, or a warning-bearing result the consumer may still use? | Return the resolved result with a warning; refusing outright removes information the consumer needs to decide for itself. | +| Are private or enterprise catalog overlays in scope, and if so with what precedence? | Defer until the shared catalog contract and its API are stable; define precedence explicitly before any SDK implementation adds overlay support. | +| Should the first contract version's composition vocabulary expand beyond [§4.5](#45-dependencies-and-composition) before implementation? | No — ship the restricted vocabulary first; expand only with an explicit, reviewed composition rule per field. | +| Who are the named MXC owners for schema/API review vs. policy/security review? | To be assigned before this document is finalized; not a contract-shape question. | + +## 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. +- [`docs/sandbox-policy/0.8.0/policy.md`](sandbox-policy/0.8.0/policy.md) — + the `SandboxPolicy` contract every catalog entry embeds. +- [`docs/versioning.md`](versioning.md) — the versioning model + [§4.1](#41-versions) builds on. From fc5fe2f256783b20b48ea379bf5dd0b56710e705 Mon Sep 17 00:00:00 2001 From: Chaz Gordish Date: Tue, 29 Sep 2026 09:59:58 -0700 Subject: [PATCH 02/12] docs: resolve policy store contract gaps Define revision migration, platform selection, dependency metadata, fail-closed composition, inspection metadata, and trust outcomes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/mxc-policy-store.md | 162 ++++++++++++++++++++++++++++----------- 1 file changed, 117 insertions(+), 45 deletions(-) diff --git a/docs/mxc-policy-store.md b/docs/mxc-policy-store.md index 960d5673a..b27c681ca 100644 --- a/docs/mxc-policy-store.md +++ b/docs/mxc-policy-store.md @@ -94,10 +94,14 @@ does not re-derive them, and cites the relevant #779 section instead. | `entryRevision` | Monotonic revision of a single entry, for cache invalidation and audit comparison. | | `sandboxPolicy.version` | The exact registered `SandboxPolicy` contract version used by one platform variant. | -These move independently. A `SandboxPolicy` version bump does not require a new -catalog revision, and a catalog revision does not require every entry to bump. -Tool version constraints (`versionRange`, below) are a fifth, orthogonal axis — -they describe which builds of the *tool* an entry was observed against, not +These identifiers serve separate purposes and do not advance in lockstep. +Registering a new `SandboxPolicy` contract does not change existing catalog +data and therefore does not require a new catalog revision. Migrating a +platform variant to that contract changes the entry's content, so publication +of that migration must increment both `entryRevision` and `catalogRevision`. +A catalog revision may still change without incrementing unaffected entries. +Tool version constraints (`versionRange`, below) are a fifth, orthogonal axis. +They describe which builds of a tool an entry was observed against, not anything about the catalog. ### 4.2 Entry shape @@ -133,10 +137,10 @@ Invariants: - `entryId` is stable, unique, namespaced, and is the only key `requires`/ dependency edges may reference. - `entryRevision` increases on every semantic change to the entry. -- Exactly one most-specific `platformVariants` entry may match a given - platform/architecture. No matching variant means the tool is unsupported on - that platform — not that it needs an empty policy, and not `undefined` - conflated with "requires nothing" (see [#779, "Defaults and +- Variant selection follows the deterministic rules in + [§4.4](#44-platform-variants). No selected variant means the tool is + unsupported on that platform, not that it needs an empty policy, and not + `undefined` conflated with "requires nothing" (see [#779, "Defaults and omission"](https://github.com/microsoft/mxc/pull/779)). - Symbols (`${project_root}`, `${npm_cache}`, OS well-known folders) are resolved by the resolver before a policy is returned; catalog data never @@ -162,12 +166,24 @@ kind). This document adds: ### 4.4 Platform variants -Supported platforms are `windows`, `linux`, and `macos`. A platform variant is -a complete requirement statement — one full `SandboxPolicy`, not a patch -applied to a base policy — plus any platform-specific dependencies. Variants -are never merged. This closes #779's open question about entries that need a -genuinely different policy per platform, not just different symbol resolution: -they now can, by declaring more than one variant. +Supported platforms are `windows`, `linux`, and `macos`. Supported architecture +selectors are `x64` and `arm64`. A variant selector has this closed shape: + +```ts +interface PlatformVariantSelector { + platform: "windows" | "linux" | "macos"; + architecture?: "x64" | "arm64"; +} +``` + +A platform variant is a complete requirement statement: one full +`SandboxPolicy`, not a patch applied to a base policy, plus any +platform-specific dependencies. Variants are never merged. Selection first +filters by `platform`, then prefers an exact `architecture` match over a +variant that omits `architecture`. Catalog validation rejects duplicate exact +selectors and more than one architecture-neutral variant for the same +platform. If neither an exact nor architecture-neutral variant exists, the +entry is unsupported on that host. A platform variant must not name a specific MXC containment backend. Policies stay backend-neutral; the selected backend still decides whether a stated @@ -175,27 +191,41 @@ requirement can be realized on that host. ### 4.5 Dependencies and composition -Dependencies reference another entry's `entryId`, with an optional tool -`versionRange`, and live inside the platform variant when platform-specific. -Resolution is transitive, cycle-rejecting, and deterministic, and the resolver -returns the full dependency chain alongside the result. +Dependencies reference another entry's `entryId` and live inside the platform +variant when platform-specific. An optional `versionRange` records which +dependency versions supplied the reviewed evidence. The v1 resolver has no +dependency inventory, so it does not evaluate that range or use it for +matching. It returns the range as unevaluated metadata for consumer inspection. +Catalog validation checks only that the range is syntactically valid. +Resolution is otherwise transitive, cycle-rejecting, and deterministic, and +the resolver returns the full dependency chain alongside the result. Unlike #779, this document does not treat "union the policies" as sufficient composition. Silently unioning arbitrary `SandboxPolicy` objects across a dependency chain hides exactly the kind of conflicting-field problem that made #779 exclude `proxy` from the embedded object. For the first contract -version, composition across a dependency chain is limited to fields where the -merge rule is unambiguous: - -- filesystem path lists, normalized and de-duplicated; -- network host lists, normalized and de-duplicated; -- boolean capability requirements, where `true` always means "this dependency - requires the capability." - -Timeout, clipboard, lifecycle, UI, and proxy fields are excluded from -cross-entry composition until each has an explicit rule; catalog validation -rejects a dependency combination that would require merging one of them, -rather than picking an implicit answer. +version, cross-entry composition is limited to the exact +`filesystem.deniedPaths`, `filesystem.readonlyPaths`, and +`filesystem.readwritePaths` fields: + +1. Every policy in the dependency closure must declare the same + `sandboxPolicy.version`. +2. Paths are resolved, normalized using the selected platform's path rules, + and de-duplicated within the same access class. +3. Catalog validation rejects equal or ancestor/descendant paths that occur in + different access classes. It never chooses between denied, read-only, and + read-write access implicitly. +4. The non-conflicting, normalized lists are merged into the returned policy. + +The v1 contract does not compose `network`. In particular, it defines no merge +for `network.egress.default`, `network.egress.allow`, +`network.egress.deny`, `network.ingress.default`, or +`network.ingress.hostLoopback`. Catalog validation rejects a dependency closure +where policies from more than one entry would require composing any `network` +field. The same rejection applies to timeout, clipboard, lifecycle, UI, proxy, +and every other policy field without an explicit cross-entry rule. Entries +without dependencies may still use catalog-supported policy fields because no +cross-entry merge occurs. ## 5. API surface @@ -216,7 +246,7 @@ interface ResolveContext { projectRoot?: string; symbols?: Record; platform?: "windows" | "linux" | "macos"; - architecture?: string; + architecture?: "x64" | "arm64"; catalogRevision?: string; allowWeakIdentityFallback?: boolean; } @@ -226,7 +256,11 @@ interface ResolvedToolEntry { entryRevision: number; catalogRevision: string; matchedIdentity: { kind: string; strength: "strong" | "weak" }; - resolvedDependencies: Array<{ entryId: string; entryRevision: number }>; + resolvedDependencies: Array<{ + entryId: string; + entryRevision: number; + requiredVersionRange?: string; + }>; policy: SandboxPolicy; warnings: string[]; } @@ -247,13 +281,39 @@ empty policy (same distinction #779 makes; see [§4.2](#42-entry-shape)). ### 5.2 Setup and inspection ```ts +type CatalogPlatform = "windows" | "linux" | "macos"; +type CatalogArchitecture = "x64" | "arm64"; + +type CatalogIdentityMetadata = + | { kind: "purl"; value: string; versionRange?: string } + | { kind: "invocation-name"; names: string[] }; + +interface CatalogEntryMetadata { + catalogRevision: string; + entryId: string; + entryRevision: number; + displayName: string; + identity: CatalogIdentityMetadata[]; + platformVariants: Array<{ + platform: CatalogPlatform; + architecture?: CatalogArchitecture; + dependencyEntryIds: string[]; + sandboxPolicyVersion: string; + }>; + provenance: { + method: string; + sourceRevision: string; + }; +} + listCatalogEntries(): CatalogEntryMetadata[]; getCatalogInfo(): { catalogSchemaVersion: string; catalogRevision: string }; ``` This supports setup UI, catalog browsing, and update decisions without paying the cost of policy resolution, and keeps "give me everything" out of the -runtime lookup path entirely. +runtime lookup path entirely. Metadata exposes selectors, dependency IDs, and +provenance, but not an unresolved or resolved policy body. ### 5.3 Consumer obligations @@ -326,20 +386,28 @@ evidence. No such hook is proposed here. ## 9. Trust model -This document tightens #779's trust framing rather than replacing it: entries -still assert *need*, not authorization, and a wrong or malicious entry can -only overstate need, which surfaces as a tool that fails under the consumer's -existing policy — never as an authority the consumer did not already grant. +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. 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, integrity-validated revision ([§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, and a consumer must still -intersect a resolved entry with its own policy rather than adopt it as policy -outright (though nothing prevents that choice — see [#779 §2.1](https://github.com/microsoft/mxc/pull/779) -for why that layering is honest about what such a choice costs). +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 @@ -366,11 +434,15 @@ have cached or recorded in an audit trail. - single tool → expected entry; unknown tool → `undefined`, never an empty policy - identity match strength selection and weak-identity fallback behavior - version-range mismatch produces a warning, not a refusal -- exactly one platform variant selected; no matching variant → `undefined` +- exact-architecture variant precedes the platform-only variant; duplicate + selectors are rejected; no matching variant produces `undefined` - dependency chain resolution, including cycles (terminate, no duplication) +- dependency `versionRange` is returned as unevaluated metadata and never used + for v1 resolver matching - restricted composition rules ([§4.5](#45-dependencies-and-composition)): - path/host de-duplication and boolean-OR merge; a dependency requiring an - unsupported composed field is rejected at validation time, not resolved + same-class path de-duplication; cross-class path overlap, mixed policy + versions, network fields, and other unsupported composed fields are rejected + at validation time rather than resolved - symbol resolution on Windows, Linux, and macOS **Data (CI)** @@ -398,7 +470,7 @@ Recommended answers are proposals for review, not decisions. | Is invocation-name-only identity accepted automatically, or does it require explicit consumer opt-in? | Treat it as a fallback requiring explicit opt-in (`allowWeakIdentityFallback`), not the default. | | What happens on a detected tool-version mismatch — `undefined`, or a warning-bearing result the consumer may still use? | Return the resolved result with a warning; refusing outright removes information the consumer needs to decide for itself. | | Are private or enterprise catalog overlays in scope, and if so with what precedence? | Defer until the shared catalog contract and its API are stable; define precedence explicitly before any SDK implementation adds overlay support. | -| Should the first contract version's composition vocabulary expand beyond [§4.5](#45-dependencies-and-composition) before implementation? | No — ship the restricted vocabulary first; expand only with an explicit, reviewed composition rule per field. | +| Should the first contract version's composition vocabulary expand beyond [§4.5](#45-dependencies-and-composition) before implementation? | No. Start with conflict-rejecting filesystem composition and expand only with an explicit, reviewed rule per field. | | Who are the named MXC owners for schema/API review vs. policy/security review? | To be assigned before this document is finalized; not a contract-shape question. | ## 14. Related work From 1709108a9541b2198445c382ea6896ada0f16145 Mon Sep 17 00:00:00 2001 From: Chaz Gordish Date: Tue, 29 Sep 2026 10:01:33 -0700 Subject: [PATCH 03/12] docs: frame policy floors as temporary stopgap Center the first-run containment problem, document the limited support horizon and Learning Mode replacement, and move catalog ownership outside MXC. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 2 +- docs/mxc-policy-store.md | 172 ++++++++++++++++++++++----------------- 2 files changed, 98 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 2b6fa7a9c..8e2097172 100644 --- a/README.md +++ b/README.md @@ -316,7 +316,7 @@ 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.md](docs/mxc-policy-store.md) | MXC Policy Store feature spec (proposed known-tool requirements catalog and resolver) | +| [docs/mxc-policy-store.md](docs/mxc-policy-store.md) | Experimental known-tool policy-floor stopgap and resolver contract | | [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.md b/docs/mxc-policy-store.md index b27c681ca..6b453fec3 100644 --- a/docs/mxc-policy-store.md +++ b/docs/mxc-policy-store.md @@ -1,30 +1,35 @@ -# Feature Spec: MXC Policy Store +# Feature Spec: Known-tool Policy Floors -**Status:** Proposed — review-ready draft, targeting sign-off around October 2, -2026. This is a proposed contract for review, not an approved, shipped, or -implemented catalog, and October 2 is a review-readiness target rather than a -delivery or personal commitment. +**Status:** Proposed experimental stopgap. This is not an approved, shipped, or +implemented catalog. + +**IMPORTANT NOTE:** This is a time-limited bridge, not a long-term supported +Microsoft product. Applying a published floor does not guarantee that a tool's +end-to-end workflow will work under process containment. The catalog and its +dedicated repository are expected to be retired when Learning Mode provides +the replacement workflow. --- ## 1. Problem Statement -[#779](https://github.com/microsoft/mxc/pull/779) proposed **config floors**: a -repository-hosted table of minimum sandbox requirements per known tool, plus an -SDK resolver, so a host does not have to discover by hand what a tool needs to -run inside a sandbox. That proposal intentionally left several things loose — -one version number for the whole table, name-only identity as the common case, -one policy per entry regardless of platform, and no distinction between "look up -one tool's requirement" and "inspect the whole catalog." - -Turning that proposal into something MXC can host and consumers can build -against requires tightening exactly those points into a contract: independently -versioned catalog and entries, ordered identity strength, complete per-platform -requirement statements, deterministic dependency resolution, a resolver API -that is safe to call in a hot path, immutable published revisions, and a -reviewed contribution pipeline. This document is that contract. It reuses -[#779](https://github.com/microsoft/mxc/pull/779)'s data model and API shape -almost entirely; where it diverges, it says so. +Developers frequently disable process isolation after enabling it breaks tools +needed for their workflow. This makes the first-run experience for process +containment poor and reduces adoption before developers can identify the +missing policy. + +The near-term mitigation is a public, reviewable set of known per-tool policy +floors. Consumers can apply a candidate floor instead of starting from no tool +knowledge, and contributors can iterate on the data as failures are found. A +floor only describes a known minimum requirement. It does not prove that every +process, dependency, credential, service, or network interaction in an +end-to-end workflow is covered. + +[#779](https://github.com/microsoft/mxc/pull/779) proposed the initial config +floor data model and SDK resolver. This document narrows that proposal into a +temporary catalog contract with explicit identity, platform, dependency, +revision, and inspection behavior. It reuses #779's model where possible and +calls out differences directly. This document does not restate general MXC sandboxing concepts already covered by [`docs/sandbox-policy/0.8.0/policy.md`](sandbox-policy/0.8.0/policy.md) or @@ -37,6 +42,8 @@ by [`docs/sandbox-policy/0.8.0/policy.md`](sandbox-policy/0.8.0/policy.md) or `SandboxPolicy`; it does not define a parallel vocabulary. - 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 @@ -44,10 +51,13 @@ by [`docs/sandbox-policy/0.8.0/policy.md`](sandbox-policy/0.8.0/policy.md) or ## 2. Ownership boundary -MXC owns exactly one thing here: an integrity-validated, versioned, read-only -catalog of known-tool sandbox requirements, and the SDK surface that resolves -it. The catalog states what a tool needs. It does not grant access, does not -modify caller state, and does not create a sandbox. +The proposed dedicated catalog project owns an integrity-validated, versioned, +read-only data set of known-tool sandbox requirements. MXC continues to own +`SandboxPolicy`. If an MXC SDK consumption API is approved, MXC also owns that +API, but not the catalog entries, repository, or publication lifecycle. + +The catalog states a candidate minimum that a tool needs. It does not grant +access, modify caller state, create a sandbox, or guarantee workflow success. Everything else is a consumer decision: @@ -61,18 +71,19 @@ Everything else is a consumer decision: sandbox creation. A catalog lookup can only ever narrow what a consumer still has to decide for -itself: MXC returns a candidate requirement or `undefined`; 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. +itself. The resolver returns a candidate requirement or `undefined`; 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 | Four independent versions: `catalogSchemaVersion`, `catalogRevision`, per-entry `entryRevision`, and per-variant `sandboxPolicy.version` ([§4.1](#41-versions)) | -| `identity` predicates, unordered | `identity` explicitly ordered strongest → weakest, with defined match/fallback behavior ([§4.3](#43-identity)) | +| One `schemaVersion` for the whole table | Four separate version dimensions: `catalogSchemaVersion`, `catalogRevision`, per-entry `entryRevision`, and per-variant `sandboxPolicy.version` ([§4.1](#41-versions)) | +| `identity` predicates, unordered | `identity` explicitly ordered strongest to weakest, with defined match/fallback behavior ([§4.3](#43-identity)) | | One `sandboxPolicy` per entry; `when.platform` only conditions dependencies | One complete `SandboxPolicy` per platform variant; a variant cannot name a containment backend ([§4.4](#44-platform-variants)) | | `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)) | | Single resolver function, no separate catalog-inspection API | Resolver split from a separate metadata/inspection API ([§5](#5-api-surface)) | @@ -146,7 +157,7 @@ Invariants: resolved by the resolver before a policy is returned; catalog data never ships a literal, machine-specific path. This is unchanged from #779. - An embedded `sandboxPolicy` is validated against the real `SandboxPolicy` - schema for its declared `version` — the catalog schema does not duplicate + schema for its declared `version`. The catalog schema does not duplicate that validation. ### 4.3 Identity @@ -162,7 +173,8 @@ kind). This document adds: what to do with the mismatch. - Invocation-name-only identity is the always-available fallback, not the default outcome. Whether a consumer accepts an invocation-name-only match - automatically, or requires opt-in, is unresolved — see [§13](#13-open-questions). + automatically, or requires opt-in, is unresolved. See + [§13](#13-open-questions). ### 4.4 Platform variants @@ -275,7 +287,7 @@ resolveCatalogEntry( a caller composing and persisting requirements per tool (rather than as one opaque merged blob) needs each result independently addressable and independently attributable. A caller resolving several tools calls it once -per tool. `undefined` means no acceptable identity/platform match — never an +per tool. `undefined` means no acceptable identity/platform match, never an empty policy (same distinction #779 makes; see [§4.2](#42-entry-shape)). ### 5.2 Setup and inspection @@ -321,16 +333,16 @@ A consumer that uses this API: 1. Decides whether automatic lookup is enabled at all. 2. Stores any accepted result per tool, keyed by `entryId`, `entryRevision`, - and `catalogRevision` — never as an unattributed merged policy blob. + and `catalogRevision`, never as an unattributed merged policy blob. 3. Keeps catalog-derived requirements in a layer separate from its own user, learned, and invocation-specific policy. 4. Applies its own authorization, elevation, and restrictive-composition rules on top. 5. Enforces its OS, enterprise, device, and backend ceilings regardless of what the catalog returned. -6. Fails closed — falls back to its own restrictive baseline, and does not - run uncontained — when a required entry cannot be realized on the current - host/backend. +6. Fails closed when a required entry cannot be realized on the current + host/backend. It falls back to its own restrictive baseline and does not + run uncontained. 7. Records matched identity, catalog/entry revision, warnings, and approval state in its own audit trail. @@ -339,20 +351,25 @@ observation (see [§8](#8-relationship-to-learning-mode)) can produce candidate evidence for a future contribution to this catalog; it is not a mechanism for mutating the catalog at request time. -## 6. Packaging and repository ownership +## 6. Intended repository and packaging boundary + +The catalog is intended to live in a new public repository outside +`microsoft/mxc`. Its schema, entries, contribution history, validation, and +publication workflow belong there. This specification remains in MXC only +while the contract and optional consumption boundary are reviewed. No catalog +repository is created by this proposal. -Canonical source lives in `microsoft/mxc`, with schema, semantic validation, -and generated package artifacts, following this repo's existing schema -codegen model ([`docs/schema-codegen.md`](schema-codegen.md)). Whether the -catalog ships inside each SDK package or as a separately versioned artifact -consumed by all SDKs is open; a separately versioned artifact is recommended -so catalog updates are not coupled to SDK release cadence. See -[§13](#13-open-questions). +MXC retains the existing `SandboxPolicy` contract. If approved, MXC may also +retain SDK types and resolver code that consume a separately versioned catalog +artifact. Catalog releases must not require an MXC SDK release, and catalog +governance must not become part of MXC repository governance. The dedicated +repository and artifact have the same limited horizon as the stopgap and may +be retired when Learning Mode replaces them. ## 7. Contribution and review -- Contributions are pull requests against `microsoft/mxc`. No client or SDK - can write a catalog entry at runtime. +- Catalog contributions are pull requests against the dedicated catalog + repository. 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. @@ -360,16 +377,21 @@ so catalog updates are not coupled to SDK release cadence. See identity uniqueness, dependency closure and cycle-freedom, symbol validity, absence of unsafe user-specific literal paths, unsupported-field rejection, deterministic resolution, and package inclusion. -- A new entry or a requirement expansion requires one MXC SDK/catalog-owner - approval and one MXC security/policy-reviewer approval, plus tool- or - scenario-owner evidence where available. +- 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 requires regression evidence that every supported tool version still functions under the narrower requirement. +- Any MXC SDK consumption change is reviewed separately in `microsoft/mxc`. ## 8. Relationship to Learning Mode -MXC's upstream learning-mode capabilities (`learningModeLogging`, -`permissiveLearningMode`, `captureDenials` — see +Learning Mode is the intended long-term solution. The known-tool catalog only +reduces immediate first-run failures while that workflow is completed. It is +not a parallel long-term policy platform. + +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 @@ -378,11 +400,11 @@ 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 into a locally-scoped policy suggestion -for its own user, is that consumer's design — most likely deferred to the -consumer, and out of scope for the catalog contract itself unless a future -revision of this contract needs to define hooks for submitting observation -evidence. No such hook is proposed here. +a candidate catalog contribution or a locally scoped policy suggestion is +that consumer's design. No runtime submission hook is proposed here. When +Learning Mode can provide the required observation and policy-authoring +experience directly, this catalog should be retired rather than promoted into +a durable platform. ## 9. Trust model @@ -411,8 +433,8 @@ 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 +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. @@ -420,18 +442,18 @@ have cached or recorded in an audit trail. - No change to `SandboxPolicy` or `ContainerConfig` schema. - No change to executor behavior. -- New SDK surface only; existing callers that never call it see no behavior - change. -- Given the schema is expected to move as open questions resolve, the catalog - and resolver should land under the experimental surface and promote through - this repo's normal promotion process once the shape has settled, per - [`docs/authoring-a-new-feature.md`](authoring-a-new-feature.md). +- Any SDK consumption surface is opt-in and experimental. Existing callers + that never call it see no behavior change. +- Catalog schema and API compatibility are limited to the stopgap's support + horizon. Retirement in favor of Learning Mode is an expected outcome, not a + normal promotion milestone. ## 12. Test plan **Resolver (SDK unit tests)** -- single tool → expected entry; unknown tool → `undefined`, never an empty policy +- single tool returns the expected entry; unknown tool returns `undefined`, + never an empty policy - identity match strength selection and weak-identity fallback behavior - version-range mismatch produces a warning, not a refusal - exact-architecture variant precedes the platform-only variant; duplicate @@ -466,22 +488,22 @@ Recommended answers are proposals for review, not decisions. | Question | Recommended answer | |---|---| -| Repository and package ownership: does the catalog ship inside each SDK package, or as a separately versioned artifact? | Canonical source in `microsoft/mxc`; prefer a separately versioned generated artifact to decouple catalog updates from SDK releases. | +| What is the dedicated repository name and owning team? | Use a public repository outside `microsoft/mxc`; publish a separately versioned artifact so catalog updates are not coupled to SDK releases. | | Is invocation-name-only identity accepted automatically, or does it require explicit consumer opt-in? | Treat it as a fallback requiring explicit opt-in (`allowWeakIdentityFallback`), not the default. | -| What happens on a detected tool-version mismatch — `undefined`, or a warning-bearing result the consumer may still use? | Return the resolved result with a warning; refusing outright removes information the consumer needs to decide for itself. | +| What happens on a detected tool-version mismatch: `undefined`, or a warning-bearing result the consumer may still use? | Return the resolved result with a warning; refusing outright removes information the consumer needs to decide for itself. | | Are private or enterprise catalog overlays in scope, and if so with what precedence? | Defer until the shared catalog contract and its API are stable; define precedence explicitly before any SDK implementation adds overlay support. | | Should the first contract version's composition vocabulary expand beyond [§4.5](#45-dependencies-and-composition) before implementation? | No. Start with conflict-rejecting filesystem composition and expand only with an explicit, reviewed rule per field. | -| Who are the named MXC owners for schema/API review vs. policy/security review? | To be assigned before this document is finalized; not a contract-shape question. | +| Who owns catalog schema/data review versus optional MXC SDK integration? | Assign catalog and security owners in the dedicated repository; keep MXC SDK review with existing MXC owners. | ## 14. Related work -- [`microsoft/mxc#779`](https://github.com/microsoft/mxc/pull/779) — Sandbox +- [`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 +- [`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. -- [`docs/sandbox-policy/0.8.0/policy.md`](sandbox-policy/0.8.0/policy.md) — +- [`docs/sandbox-policy/0.8.0/policy.md`](sandbox-policy/0.8.0/policy.md) - the `SandboxPolicy` contract every catalog entry embeds. -- [`docs/versioning.md`](versioning.md) — the versioning model +- [`docs/versioning.md`](versioning.md) - the versioning model [§4.1](#41-versions) builds on. From dd575bd8c28cf7aa6764615abcad06727fa0b248 Mon Sep 17 00:00:00 2001 From: Chaz Gordish Date: Tue, 29 Sep 2026 10:12:26 -0700 Subject: [PATCH 04/12] docs: align policy floor spec with feature guide Add an in-place feature-impact and omission-defaults subsection without restructuring the reviewed document. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/mxc-policy-store.md | 44 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/docs/mxc-policy-store.md b/docs/mxc-policy-store.md index 6b453fec3..c806e6b37 100644 --- a/docs/mxc-policy-store.md +++ b/docs/mxc-policy-store.md @@ -49,6 +49,42 @@ by [`docs/sandbox-policy/0.8.0/policy.md`](sandbox-policy/0.8.0/policy.md) or - This does not define Learning Mode's candidate-generation or review UX. See [§8](#8-relationship-to-learning-mode). +### MXC feature impact and defaults + +This proposal follows the SDK-only path in +[`docs/authoring-a-new-feature.md`](authoring-a-new-feature.md): + +- **Policy changes:** None. Catalog entries embed an existing, registered + `SandboxPolicy`. +- **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. +- **SDK changes:** An optional catalog-consumption API may be added after its + ownership and packaging boundary are approved. + +The word **experimental** in this document describes the catalog's support +horizon. It does not add an MXC schema feature, activate the +`--experimental` runtime gate, or change executor behavior. + +Defaults and omission behavior are: + +- Existing callers do not perform catalog lookup automatically. A consumer + must explicitly enable or invoke it. +- Omitted `ResolveContext.platform` and `ResolveContext.architecture` use the + current host values. +- Omitted `ResolveContext.catalogRevision` uses the currently installed + catalog revision. +- Omitted `ResolveContext.allowWeakIdentityFallback` is `false`. +- Omitted `projectRoot` and `symbols` provide no caller overrides. The resolver + may use approved host-known symbols, but it does not invent machine-specific + values. A selected entry with an unresolved required symbol is not + resolvable. +- Omitted `packageUrl` or `detectedVersion` supplies no matching evidence. The + resolver does not fabricate either value. +- No acceptable or resolvable match returns `undefined`. The consumer's + restrictive baseline remains unchanged. + ## 2. Ownership boundary The proposed dedicated catalog project owns an integrity-validated, versioned, @@ -287,8 +323,9 @@ resolveCatalogEntry( a caller composing and persisting requirements per tool (rather than as one opaque merged blob) needs each result independently addressable and independently attributable. A caller resolving several tools calls it once -per tool. `undefined` means no acceptable identity/platform match, never an -empty policy (same distinction #779 makes; see [§4.2](#42-entry-shape)). +per tool. `undefined` means no acceptable or resolvable identity/platform +match, never an empty policy (same distinction #779 makes; see +[§4.2](#42-entry-shape)). ### 5.2 Setup and inspection @@ -454,6 +491,9 @@ have cached or recorded in an audit trail. - single tool returns the expected entry; unknown tool returns `undefined`, never an empty policy +- omitted context uses host platform/architecture, the installed catalog + revision, no caller symbol overrides, and no weak-identity fallback +- an unresolved required symbol returns `undefined`, never a partial policy - identity match strength selection and weak-identity fallback behavior - version-range mismatch produces a warning, not a refusal - exact-architecture variant precedes the platform-only variant; duplicate From 324235e545140bc98589a2c61fbffabcbf288fde Mon Sep 17 00:00:00 2001 From: Chaz Gordish Date: Tue, 29 Sep 2026 13:42:40 -0700 Subject: [PATCH 05/12] docs: describe policy catalog as proposed public preview Use the selected support designation while retaining the limited lifetime, proposed SDK boundary, and not-shipped disclaimer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 2 +- docs/mxc-policy-store.md | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8e2097172..2272cd183 100644 --- a/README.md +++ b/README.md @@ -316,7 +316,7 @@ 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.md](docs/mxc-policy-store.md) | Experimental known-tool policy-floor stopgap and resolver contract | +| [docs/mxc-policy-store.md](docs/mxc-policy-store.md) | Proposed public preview known-tool policy catalog and resolver contract | | [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.md b/docs/mxc-policy-store.md index c806e6b37..2e0877dc1 100644 --- a/docs/mxc-policy-store.md +++ b/docs/mxc-policy-store.md @@ -1,6 +1,6 @@ # Feature Spec: Known-tool Policy Floors -**Status:** Proposed experimental stopgap. This is not an approved, shipped, or +**Status:** Proposed public preview catalog. This is not an approved, shipped, or implemented catalog. **IMPORTANT NOTE:** This is a time-limited bridge, not a long-term supported @@ -63,8 +63,8 @@ This proposal follows the SDK-only path in - **SDK changes:** An optional catalog-consumption API may be added after its ownership and packaging boundary are approved. -The word **experimental** in this document describes the catalog's support -horizon. It does not add an MXC schema feature, activate the +The proposed **public preview** designation describes the catalog's support +status. It does not add an MXC schema feature, activate the `--experimental` runtime gate, or change executor behavior. Defaults and omission behavior are: @@ -479,8 +479,8 @@ have cached or recorded in an audit trail. - No change to `SandboxPolicy` or `ContainerConfig` schema. - No change to executor behavior. -- Any SDK consumption surface is opt-in and experimental. Existing callers - that never call it see no behavior change. +- Any SDK consumption surface is opt-in and remains subject to approval. + Existing callers that never call it see no behavior change. - Catalog schema and API compatibility are limited to the stopgap's support horizon. Retirement in favor of Learning Mode is an expected outcome, not a normal promotion milestone. From 34537f85384c00f72564f931077d1790c5a86a02 Mon Sep 17 00:00:00 2001 From: Chaz Gordish Date: Tue, 29 Sep 2026 14:59:50 -0700 Subject: [PATCH 06/12] docs: define standalone policy catalog libraries Remove speculative MXC SDK integration and describe standalone TypeScript/JavaScript, Rust, and .NET libraries, local catalog consumption, and cross-language conformance. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 2 +- docs/mxc-policy-store.md | 147 ++++++++++++++++++++++++++++++--------- 2 files changed, 116 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 2272cd183..243ffe80b 100644 --- a/README.md +++ b/README.md @@ -316,7 +316,7 @@ 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.md](docs/mxc-policy-store.md) | Proposed public preview known-tool policy catalog and resolver contract | +| [docs/mxc-policy-store.md](docs/mxc-policy-store.md) | Proposed public preview known-tool policy catalog and standalone libraries | | [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.md b/docs/mxc-policy-store.md index 2e0877dc1..3e78197e1 100644 --- a/docs/mxc-policy-store.md +++ b/docs/mxc-policy-store.md @@ -51,8 +51,8 @@ by [`docs/sandbox-policy/0.8.0/policy.md`](sandbox-policy/0.8.0/policy.md) or ### MXC feature impact and defaults -This proposal follows the SDK-only path in -[`docs/authoring-a-new-feature.md`](authoring-a-new-feature.md): +This is a standalone catalog and library proposal. Following the feature-impact +checklist in [`docs/authoring-a-new-feature.md`](authoring-a-new-feature.md): - **Policy changes:** None. Catalog entries embed an existing, registered `SandboxPolicy`. @@ -60,8 +60,11 @@ This proposal follows the SDK-only path in 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. -- **SDK changes:** An optional catalog-consumption API may be added after its - ownership and packaging boundary are approved. +- **MXC SDK changes:** None. This proposal does not add catalog lookup, types, + or resolver code to the MXC SDKs. +- **Standalone libraries:** The dedicated catalog repository provides its own + library API for TypeScript/JavaScript, Rust, and C#/.NET. See + [§6](#6-intended-repository-and-packaging-boundary). The proposed **public preview** designation describes the catalog's support status. It does not add an MXC schema feature, activate the @@ -88,9 +91,10 @@ Defaults and omission behavior are: ## 2. Ownership boundary The proposed dedicated catalog project owns an integrity-validated, versioned, -read-only data set of known-tool sandbox requirements. MXC continues to own -`SandboxPolicy`. If an MXC SDK consumption API is approved, MXC also owns that -API, but not the catalog entries, repository, or publication lifecycle. +read-only data set of known-tool sandbox requirements, its resolver libraries, +and their public APIs. MXC continues to own the existing `SandboxPolicy` +contract, but not the catalog entries, libraries, repository, or publication +lifecycle. The catalog states a candidate minimum that a tool needs. It does not grant access, modify caller state, create a sandbox, or guarantee workflow success. @@ -277,9 +281,15 @@ cross-entry merge occurs. ## 5. API surface -Two APIs, kept deliberately separate so that "resolve one tool's requirement" -stays a cheap, hot-path-safe call and never implicitly returns the whole -catalog. +The standalone libraries expose two API groups, kept deliberately separate so +that "resolve one tool's requirement" stays a cheap, hot-path-safe call and +never implicitly returns the whole catalog. These are in-process library +calls, not a hosted service or additions to the MXC SDKs. + +The signatures below use TypeScript to describe the shared contract. Rust and +C# expose the same operations and metadata with idiomatic names and types. +A no-match result is `undefined` in TypeScript/JavaScript, `None` in Rust, and +`null` in C#. Library failures remain distinct from a no-match result. ### 5.1 Runtime lookup @@ -383,25 +393,85 @@ A consumer that uses this API: 7. Records matched identity, catalog/entry revision, warnings, and approval state in its own audit trail. -MXC never writes a consumer's policy store. A consumer's own capability -observation (see [§8](#8-relationship-to-learning-mode)) can produce candidate -evidence for a future contribution to this catalog; it is not a mechanism for -mutating the catalog at request time. +The catalog libraries never write a consumer's policy store. A consumer's own +capability observation (see [§8](#8-relationship-to-learning-mode)) can produce +candidate evidence for a future contribution to this catalog; it is not a +mechanism for mutating the catalog at request time. ## 6. Intended repository and packaging boundary The catalog is intended to live in a new public repository outside -`microsoft/mxc`. Its schema, entries, contribution history, validation, and -publication workflow belong there. This specification remains in MXC only -while the contract and optional consumption boundary are reviewed. No catalog -repository is created by this proposal. - -MXC retains the existing `SandboxPolicy` contract. If approved, MXC may also -retain SDK types and resolver code that consume a separately versioned catalog -artifact. Catalog releases must not require an MXC SDK release, and catalog -governance must not become part of MXC repository governance. The dedicated -repository and artifact have the same limited horizon as the stopgap and may -be retired when Learning Mode replaces them. +`microsoft/mxc`. Its schema, entries, resolver libraries, contribution history, +validation, and publication workflow belong there. This specification remains +in MXC while the proposed contract is reviewed. No catalog repository or +package is created by this proposal. + +MXC retains the existing `SandboxPolicy` contract. The catalog libraries +produce policy data conforming to that contract; they do not require an MXC +executor or execution library to perform lookup. A consumer that uses MXC +passes its final, authorized policy to an existing MXC SDK separately. +Catalog and library releases do not require an MXC SDK release or changes to +MXC repository governance. + +### 6.1 Library distribution and consumption + +The initial library language coverage matches MXC's current first-party SDK +languages, but the packages are owned and released by the catalog project: + +| Language | Distribution | API form | +|---|---|---| +| TypeScript / JavaScript | npm package | JavaScript library with TypeScript declarations | +| Rust | Cargo crate | Public Rust library API | +| C# / .NET | NuGet package | Managed library API | + +Repository and package names remain to be selected. This language match does +not require copying MXC's native-binding architecture or exposing sandbox +execution operations. + +A consumer: + +1. Installs and pins the standalone library package for its language. Each + package includes a reviewed default catalog revision for local use. +2. Calls `getCatalogInfo()` or `listCatalogEntries()` for inspection, or + `resolveCatalogEntry()` for one tool, using the corresponding language API. +3. Handles no match without widening its restrictive baseline. For a match, + it reviews the returned policy, identity, revisions, and warnings and + applies the consumer obligations in [§5.3](#53-consumer-obligations). +4. Supplies its final policy to its chosen execution integration. The catalog + library does not launch a sandbox. + +Lookup is local and does not download updates, contact a hosted service, or +run the candidate tool. `ResolveContext.catalogRevision` selects an available +local revision, not a network lookup; an explicitly requested revision that +is unavailable is an error, not a substitution with a different revision. +An omitted revision uses the library's installed default. + +Catalog revisions are also published as immutable, language-neutral data +artifacts. A library package version identifies the library release, not the +catalog revision or embedded `SandboxPolicy.version`; it declares the catalog +schema and policy versions it supports and reports its bundled +`catalogRevision`. Publishing newer data can update the packages' bundled +revision without changing resolver behavior. Installing an update does not +rewrite a consumer's previously accepted per-tool policies. + +### 6.2 Cross-language consistency and support + +All three libraries use the same catalog format and shared conformance +fixtures. Given the same catalog revision, candidate, and explicit resolution +context, they must agree on matching, variant selection, dependency metadata, +resolved policy, warnings, and failure categories. Language-specific absence +and error types must preserve those distinctions. + +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. Implementation sharing between languages is a separate +engineering decision, not a requirement to depend on MXC's engine. + +Supporting three languages includes maintaining parity, dependencies, +documentation, and releases, not only writing the initial implementations. +The libraries and catalog have the same limited public-preview horizon and +are intended to retire together when Learning Mode replaces this workflow. ## 7. Contribution and review @@ -419,7 +489,10 @@ be retired when Learning Mode replaces them. evidence where available. - A requirement reduction requires regression evidence that every supported tool version still functions under the narrower requirement. -- Any MXC SDK consumption change is reviewed separately in `microsoft/mxc`. +- Library API and implementation contributions are reviewed in the dedicated + catalog repository. These contribution requirements do not require + applications to seek maintainer approval to use the public catalog or + libraries. ## 8. Relationship to Learning Mode @@ -479,18 +552,21 @@ have cached or recorded in an audit trail. - No change to `SandboxPolicy` or `ContainerConfig` schema. - No change to executor behavior. -- Any SDK consumption surface is opt-in and remains subject to approval. - Existing callers that never call it see no behavior change. +- No change to the MXC SDK APIs or dependencies. Catalog lookup requires an + explicit call to a standalone library; existing MXC callers see no behavior + change. - Catalog schema and API compatibility are limited to the stopgap's support horizon. Retirement in favor of Learning Mode is an expected outcome, not a normal promotion milestone. ## 12. Test plan -**Resolver (SDK unit tests)** +**Resolver libraries (TypeScript/JavaScript, Rust, and C#/.NET)** +- shared conformance fixtures produce equivalent results and failure + categories in all three languages - single tool returns the expected entry; unknown tool returns `undefined`, - never an empty policy + or the language-equivalent absence value, never an empty policy - omitted context uses host platform/architecture, the installed catalog revision, no caller symbol overrides, and no weak-identity fallback - an unresolved required symbol returns `undefined`, never a partial policy @@ -517,6 +593,12 @@ have cached or recorded in an audit trail. **Integration** +- each package installs and performs lookup without an MXC executor or + execution library; lookup requires no network access +- the bundled catalog revision matches `getCatalogInfo()`; selecting an + unavailable revision fails explicitly, without falling back to another + revision +- a package 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 same tool still fails when the consumer's policy forbids what the entry @@ -531,9 +613,10 @@ Recommended answers are proposals for review, not decisions. | What is the dedicated repository name and owning team? | Use a public repository outside `microsoft/mxc`; publish a separately versioned artifact so catalog updates are not coupled to SDK releases. | | Is invocation-name-only identity accepted automatically, or does it require explicit consumer opt-in? | Treat it as a fallback requiring explicit opt-in (`allowWeakIdentityFallback`), not the default. | | What happens on a detected tool-version mismatch: `undefined`, or a warning-bearing result the consumer may still use? | Return the resolved result with a warning; refusing outright removes information the consumer needs to decide for itself. | -| Are private or enterprise catalog overlays in scope, and if so with what precedence? | Defer until the shared catalog contract and its API are stable; define precedence explicitly before any SDK implementation adds overlay support. | +| Are private or enterprise catalog overlays in scope, and if so with what precedence? | Defer until the shared catalog contract and its API are stable; define precedence explicitly before any library implementation adds overlay support. | | Should the first contract version's composition vocabulary expand beyond [§4.5](#45-dependencies-and-composition) before implementation? | No. Start with conflict-rejecting filesystem composition and expand only with an explicit, reviewed rule per field. | -| Who owns catalog schema/data review versus optional MXC SDK integration? | Assign catalog and security owners in the dedicated repository; keep MXC SDK review with existing MXC owners. | +| Who owns catalog schema, data, and library API review? | Assign catalog, library, and security reviewers in the dedicated repository; no MXC SDK integration is proposed. | +| Should the libraries share a resolver implementation or implement the contract independently? | Choose based on dependency footprint and maintenance cost, with shared conformance fixtures required either way. | ## 14. Related work From ed8a75c01f9c69f0a55fe9d7cdc2af39a764b42f Mon Sep 17 00:00:00 2001 From: Chaz Gordish Date: Wed, 30 Sep 2026 21:30:31 -0700 Subject: [PATCH 07/12] docs: refine policy catalog API and resolution contract Restore single-tool and multi-tool policy APIs, define additive matching and filesystem floor composition, reuse MXC SDK policy types, and clarify symbol discovery, casing, diagnostics, integrity, and language consistency. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/mxc-policy-store.md | 617 +++++++++++++++++++++++++++++++-------- 1 file changed, 499 insertions(+), 118 deletions(-) diff --git a/docs/mxc-policy-store.md b/docs/mxc-policy-store.md index 3e78197e1..f76e4bfbb 100644 --- a/docs/mxc-policy-store.md +++ b/docs/mxc-policy-store.md @@ -63,7 +63,9 @@ checklist in [`docs/authoring-a-new-feature.md`](authoring-a-new-feature.md): - **MXC SDK changes:** None. This proposal does not add catalog lookup, types, or resolver code to the MXC SDKs. - **Standalone libraries:** The dedicated catalog repository provides its own - library API for TypeScript/JavaScript, Rust, and C#/.NET. See + library API for TypeScript/JavaScript, Rust, and C#/.NET, each depending on + the corresponding MXC SDK and returning its existing `SandboxPolicy` type. + MXC does not depend on the catalog. See [§6](#6-intended-repository-and-packaging-boundary). The proposed **public preview** designation describes the catalog's support @@ -74,19 +76,25 @@ Defaults and omission behavior are: - Existing callers do not perform catalog lookup automatically. A consumer must explicitly enable or invoke it. -- Omitted `ResolveContext.platform` and `ResolveContext.architecture` use the - current host values. +- Omitted `ResolveContext.platform` uses the current host platform. +- Omitted `ResolveContext.architecture` uses the device's native system + architecture, not the architecture of the library's process or a detected + tool build. Explicit caller selection takes precedence. See the selection + rules and emulation risk in [§4.4](#44-platform-variants). - Omitted `ResolveContext.catalogRevision` uses the currently installed catalog revision. - Omitted `ResolveContext.allowWeakIdentityFallback` is `false`. - Omitted `projectRoot` and `symbols` provide no caller overrides. The resolver - may use approved host-known symbols, but it does not invent machine-specific - values. A selected entry with an unresolved required symbol is not - resolvable. + uses supported local discovery and shared documented defaults for required + symbols as specified in [§4.2](#42-entry-shape). It does not invent a project + root or an installation path. A selected entry with an unresolved required + symbol is not resolvable. - Omitted `packageUrl` or `detectedVersion` supplies no matching evidence. The resolver does not fabricate either value. -- No acceptable or resolvable match returns `undefined`. The consumer's - restrictive baseline remains unchanged. +- If no policy can be resolved, `getSandboxConfig` returns `undefined`. + `getSandboxConfigWithDiagnostics` instead returns a result whose `policy` is + `undefined`, preserving the diagnostics. The consumer's restrictive baseline + remains unchanged. ## 2. Ownership boundary @@ -98,12 +106,15 @@ lifecycle. The catalog states a candidate minimum that a tool needs. It does not grant access, modify caller state, create a sandbox, or guarantee workflow success. +Filesystem composition combines 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 tool, which catalog/entry +- 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. @@ -111,9 +122,10 @@ Everything else is a consumer decision: sandbox creation. A catalog lookup can only ever narrow what a consumer still has to decide for -itself. The resolver returns a candidate requirement or `undefined`; the -consumer decides whether and how to act on it. This mirrors #779's floor/policy -distinction, discussed further in +itself. `getSandboxConfig` returns a candidate composed requirement 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. @@ -123,16 +135,16 @@ required to grant. | #779 (config floors) | This document (policy store) | |---|---| | One `schemaVersion` for the whole table | Four separate version dimensions: `catalogSchemaVersion`, `catalogRevision`, per-entry `entryRevision`, and per-variant `sandboxPolicy.version` ([§4.1](#41-versions)) | -| `identity` predicates, unordered | `identity` explicitly ordered strongest to weakest, with defined match/fallback behavior ([§4.3](#43-identity)) | +| Strongest satisfied identity predicate describes a match | All eligible matching entries contribute; identity evidence is retained without stronger matches suppressing weaker ones ([§4.3](#43-identity)) | | One `sandboxPolicy` per entry; `when.platform` only conditions dependencies | One complete `SandboxPolicy` per platform variant; a variant cannot name a containment backend ([§4.4](#44-platform-variants)) | | `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)) | -| Single resolver function, no separate catalog-inspection API | Resolver split from a separate metadata/inspection API ([§5](#5-api-surface)) | +| `getSandboxConfigForTool(tools: string[])` returns one composed policy | `getSandboxConfig` accepts one tool or an array and returns one policy; `getSandboxConfigWithDiagnostics` adds attribution, with catalog inspection kept separate ([§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, the identity layering -problem (invocation name vs. launcher artifact vs. executing image), and the -trust framing all carry forward from #779 essentially unchanged; this document -does not re-derive them, and cites the relevant #779 section instead. +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 @@ -200,12 +212,86 @@ Invariants: schema for its declared `version`. The catalog schema does not duplicate that validation. +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 `SandboxPolicy`. 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" + } + } + } +} +``` + +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 +covered by the catalog revision's integrity validation and cannot change +behind a pinned revision. + +For symbols required by selected entries and dependencies, precedence is: + +1. Explicit caller values from `projectRoot` or `symbols`, as applicable. +2. Supported local discovery, including `PATH`, known host locations, and + relevant tool configuration overrides. +3. A documented default for the selected platform, if applicable. +4. Unresolved, with diagnostics and no partial policy. + +Tool-specific discovery runs in the library, not in catalog-supplied code. +It does not execute candidate tools, install software, or contact the network. +Automatic discovery and host-derived values describe the current host and +environment; callers targeting another execution environment supply overrides. +A failed configuration read is an explicit library error, not evidence that +no override exists and a default should be used. Discovery does not verify +tool identity. The diagnostics operation reports the source and resolved value +of each discovered or defaulted symbol through `diagnostics.warnings`. + ### 4.3 Identity -`identity` is an ordered list, strongest predicate first, per the layering -#779 §3.1 establishes (invocation name vs. launcher artifact vs. executing -image; falsifiable-against-a-local-artifact as the admission test for a new -kind). This document adds: +`identity` describes the predicates a candidate can satisfy for an entry, +using the layering #779 §3.1 establishes (invocation name vs. launcher artifact +vs. executing image; falsifiable-against-a-local-artifact as the admission test +for a new kind). + +Caller-supplied identity is not verified identity. A `packageUrl` match does +not prove that the installed tool belongs to that package; the caller is +responsible for verifying that association. The library does not inspect the +tool to verify it. + +Invocation-name matching is locale-independent and case-insensitive on every +platform. This affects catalog lookup only: it does not change the caller's +command, the OS's executable lookup rules, or package-identity matching. + +Matching is additive across entries. For each input tool, the resolver collects +every entry with a satisfied, eligible identity predicate and an applicable +platform/architecture variant. It does not choose a single winning entry: + +- A package-identity match does not suppress another entry's eligible + invocation-name match. Equal-strength matches to different entries also + contribute; they are not ambiguity errors. +- Identity strength describes the matching evidence, not precedence between + entries. Diagnostics retain all satisfied identity predicates for each + contributing entry. +- Multiple predicates matching the same entry do not add its policy multiple + times. Entries shared across input tools or dependency chains likewise + contribute once, while diagnostics preserve the per-input matches. +- Multiple matching entries for one input produce a diagnostic warning, not + a refusal. Their policies must still satisfy the composition rules in + [§4.5](#45-dependencies-and-composition). +- Diagnostic tool records follow input order, matches are ordered by + `entryId`, and matched predicates follow their declaration order within the + entry. Catalog file order does not select or exclude a match. + +The existing matching qualifications remain: - A version range on an identity predicate is advisory matching evidence, not a gate. A detected mismatch returns a diagnostic alongside the resolved @@ -214,7 +300,10 @@ kind). This document adds: - Invocation-name-only identity is the always-available fallback, not the default outcome. Whether a consumer accepts an invocation-name-only match automatically, or requires opt-in, is unresolved. See - [§13](#13-open-questions). + [§13](#13-open-questions). Under the current proposed default, an entry + matched only by invocation name participates when + `allowWeakIdentityFallback` is `true`. Compose-all-matches does not bypass + that option. ### 4.4 Platform variants @@ -230,12 +319,43 @@ interface PlatformVariantSelector { A platform variant is a complete requirement statement: one full `SandboxPolicy`, not a patch applied to a base policy, plus any -platform-specific dependencies. Variants are never merged. Selection first -filters by `platform`, then prefers an exact `architecture` match over a -variant that omits `architecture`. Catalog validation rejects duplicate exact -selectors and more than one architecture-neutral variant for the same -platform. If neither an exact nor architecture-neutral variant exists, the -entry is unsupported on that host. +platform-specific dependencies. Variants are never merged. Architecture is a +catalog selector, not a field added to the embedded `SandboxPolicy`. Omitting +`when.architecture` makes a catalog variant architecture-neutral; omitting the +caller's `ResolveContext.architecture` instead requests the host default. + +Selection first filters by platform, then uses the following precedence: + +| Caller context | Preferred variant | Fallback | +|---|---|---| +| Explicit `architecture: "x64"` | x64 for the selected platform | Architecture-neutral for that platform | +| Explicit `architecture: "arm64"` | ARM64 for the selected platform | Architecture-neutral for that platform | +| Architecture omitted | Device's native system architecture for the selected platform | Architecture-neutral for that platform | + +The native system architecture is the architecture reported by the host OS, +not the architecture of the process hosting the library. For example, on an +ARM64 device with both x64 and ARM64 catalog variants and no neutral variant, +omitting architecture selects ARM64. An explicit `architecture: "x64"` selects +x64 on that same device. The resolver does not require a neutral variant to +return a result when the effective architecture has an exact match. + +Catalog validation rejects duplicate exact selectors and more than one +architecture-neutral variant for the same platform. If neither an exact nor +architecture-neutral variant exists, that entry contributes no match, not an +empty policy or a variant for a different architecture. A failure to determine +the native system architecture when it is needed is a library error, not a +guessed selection. + +**Emulation risk:** A host-derived default does not establish the architecture +of the installed tool. An x64 tool running under emulation on an ARM64 device +may need the x64 variant rather than the default ARM64 variant. The resolver +does not inspect or run the tool to discover its architecture. Callers that +know the relevant tool and runtime requirements should select architecture +explicitly and remain responsible for deciding whether the result applies. +Neither explicit selection nor a host default guarantees that the returned +policy is sufficient or minimal. Host-derived selection and neutral fallback +are surfaced through `diagnostics.warnings` by the diagnostics API +([§5.1](#51-runtime-lookup)). A platform variant must not name a specific MXC containment backend. Policies stay backend-neutral; the selected backend still decides whether a stated @@ -250,56 +370,126 @@ dependency inventory, so it does not evaluate that range or use it for matching. It returns the range as unevaluated metadata for consumer inspection. Catalog validation checks only that the range is syntactically valid. Resolution is otherwise transitive, cycle-rejecting, and deterministic, and -the resolver returns the full dependency chain alongside the result. - -Unlike #779, this document does not treat "union the policies" as sufficient -composition. Silently unioning arbitrary `SandboxPolicy` objects across a -dependency chain hides exactly the kind of conflicting-field problem that -made #779 exclude `proxy` from the embedded object. For the first contract -version, cross-entry composition is limited to the exact +the diagnostics API returns the resolved dependency metadata alongside the +policy. + +The same composition rules apply to all entries matched by one tool, entries +matched by different tools in an array, and their transitive dependencies. +Each selected entry contributes its policy once, even when reached through +multiple inputs or dependency edges. Repeated contribution is de-duplicated +by entry ID within the selected catalog revision, not by discarding match +attribution. A shared `ResolveContext` applies to the whole lookup. + +Following #779's floor semantics, filesystem composition preserves the access +required by all selected tools rather than intersecting their requirements. +An overlapping read-only requirement must not suppress another tool's needed +write access, and a catalog-provided deny must not block a required read or +write path. This is not a rule for merging consumer authorization policy. +For the first contract version, cross-entry composition is limited to the exact `filesystem.deniedPaths`, `filesystem.readonlyPaths`, and `filesystem.readwritePaths` fields: -1. Every policy in the dependency closure must declare the same - `sandboxPolicy.version`. -2. Paths are resolved, normalized using the selected platform's path rules, - and de-duplicated within the same access class. -3. Catalog validation rejects equal or ancestor/descendant paths that occur in - different access classes. It never chooses between denied, read-only, and - read-write access implicitly. -4. The non-conflicting, normalized lists are merged into the returned policy. +1. Every policy in the selected entries and their dependency closure must + declare the same `sandboxPolicy.version`. +2. At lookup time, resolve all required symbols and normalize paths using the + selected platform's path rules before comparing equal or + ancestor/descendant paths. Combine the selected entries and dependencies, + de-duplicating paths within each access class. +3. Preserve every required read-write subtree. Remove read-only entries equal + to or contained within a read-write subtree, since read-write already + satisfies their read requirement. Retain a read-only ancestor of a + read-write subtree without promoting the whole ancestor to read-write. +4. Remove each catalog-provided deny that overlaps any required read-only or + read-write path, whether equal, an ancestor, or a descendant. Remove the + entire deny entry, not an invented exception beneath it. Retain + non-overlapping denies. +5. Return the composed policy and make the access changes available through + diagnostics. The same rules apply to overlaps within one selected entry + and across multiple entries; matching or traversal order must not change + the effective access. + +Filesystem comparison is separate from invocation-name matching. Equality, +de-duplication, and ancestor checks honor the applicable filesystem and +directory case-sensitivity, not a blanket OS assumption. When that information +cannot be determined, compare case-sensitively, preserve differently cased +paths as distinct, and report the assumption in diagnostics. Returned paths +retain their casing; comparison must not lowercase the policy paths. Another +target environment must not inherit this host's filesystem case rules. + +| Resolved requirements | Composed filesystem policy | +|---|---| +| Read-only `/work` and read-write `/work` | Read-write `/work`; omit read-only `/work` | +| Read-write `/work` and read-only `/work/tools` | Read-write `/work`; omit read-only `/work/tools` | +| Read-only `/work` and read-write `/work/cache` | Retain read-only `/work` and read-write `/work/cache`; do not make all of `/work` writable | +| Denied `/data` and read-write `/data/cache` | Remove denied `/data`; retain read-write `/data/cache` and report that the entire `/data` deny was removed | +| Denied `/secrets` and read-write `/work` | Retain both non-overlapping entries | + +These are composition rules for the returned MXC `SandboxPolicy`, not changes +to MXC's enforcement precedence. Simply concatenating a conflicting deny or +read-only entry with a grant is insufficient: the restrictive entry could +still prevent the access the composed floor is intended to request. + +Removing a parent deny removes its protection for the entire subtree, not +just the overlapping required path. It does not itself add a grant to that +subtree, but other grants can now apply there. Diagnostics must identify the +removed deny and this broader effect. Caller-owned denies and other user, +enterprise, device, or backend restrictions are never inputs to this +least-restrictive catalog composition and must not be removed by it. + +Publication checks validate policy shapes, symbols, and supported composition +fields and exercise the rules with known paths and fixtures. Equal or nested +filesystem requirements are not by themselves invalid catalog data. Caller +symbol values can introduce additional overlaps, so the resolver must always +apply these rules after substitution and normalization at lookup time. +An overlap covered by these rules is not a composition error; missing required +symbols or unsupported composed fields retain their existing failure behavior. The v1 contract does not compose `network`. In particular, it defines no merge for `network.egress.default`, `network.egress.allow`, `network.egress.deny`, `network.ingress.default`, or -`network.ingress.hostLoopback`. Catalog validation rejects a dependency closure +`network.ingress.hostLoopback`. Composition rejects a selected set of entries where policies from more than one entry would require composing any `network` field. The same rejection applies to timeout, clipboard, lifecycle, UI, proxy, -and every other policy field without an explicit cross-entry rule. Entries -without dependencies may still use catalog-supported policy fields because no -cross-entry merge occurs. +and every other policy field without an explicit cross-entry rule. A lookup +resolving to only one entry without dependencies may still use +catalog-supported policy fields because no cross-entry merge occurs. ## 5. API surface -The standalone libraries expose two API groups, kept deliberately separate so -that "resolve one tool's requirement" stays a cheap, hot-path-safe call and -never implicitly returns the whole catalog. These are in-process library -calls, not a hosted service or additions to the MXC SDKs. +The standalone libraries separate runtime resolution from catalog inspection. +Resolution accepts one tool or an array and composes all applicable matching +entries and dependencies into one `SandboxPolicy`. Callers choose a policy-only +operation or a diagnostic operation over the same resolution logic. Neither +implicitly returns the whole catalog. These are in-process library calls, not +a hosted service or additions to the MXC SDKs. The signatures below use TypeScript to describe the shared contract. Rust and C# expose the same operations and metadata with idiomatic names and types. -A no-match result is `undefined` in TypeScript/JavaScript, `None` in Rust, and -`null` in C#. Library failures remain distinct from a no-match result. +TypeScript and C# expose single-tool and array overloads; Rust uses an idiomatic +one-or-many input type because it does not support function overloading. An +absent policy is `undefined` in TypeScript/JavaScript, `None` in Rust, and +`null` in C#. Library failures remain distinct from policy absence. ### 5.1 Runtime lookup +Failures reuse existing MXC error codes, which are sufficient for normal +programmatic handling. An optional `details.reason` may provide a stable, +catalog-specific distinction for logging, investigation, or finer handling +when the code alone is too broad. Callers need not branch on it; an absent or +unrecognized reason retains the same handling as the primary code. A reason +must not duplicate a distinction already expressed by an existing MXC code. + ```ts +import type { SandboxPolicy } from "@microsoft/mxc-sdk"; + interface ToolCandidate { invocationName: string; packageUrl?: string; detectedVersion?: string; } +type ToolInput = string | ToolCandidate; + interface ResolveContext { projectRoot?: string; symbols?: Record; @@ -309,33 +499,111 @@ interface ResolveContext { allowWeakIdentityFallback?: boolean; } -interface ResolvedToolEntry { - entryId: string; - entryRevision: number; - catalogRevision: string; - matchedIdentity: { kind: string; strength: "strong" | "weak" }; - resolvedDependencies: Array<{ - entryId: string; - entryRevision: number; - requiredVersionRange?: string; - }>; - policy: SandboxPolicy; - warnings: string[]; +interface SandboxConfigResolution { + policy: SandboxPolicy | undefined; + diagnostics: { + catalogRevision: string; + tools: Array<{ + inputIndex: number; + matches: Array<{ + entryId: string; + entryRevision: number; + matchedIdentities: Array<{ + kind: string; + strength: "strong" | "weak"; + }>; + }>; + }>; + resolvedDependencies: Array<{ + entryId: string; + entryRevision: number; + requiredVersionRange?: string; + }>; + warnings: string[]; + }; } -resolveCatalogEntry( - tool: ToolCandidate, +declare function getSandboxConfig( + tool: ToolInput, + ctx?: ResolveContext +): SandboxPolicy | undefined; + +declare function getSandboxConfig( + tools: readonly ToolInput[], ctx?: ResolveContext -): ResolvedToolEntry | undefined; +): SandboxPolicy | undefined; + +declare function getSandboxConfigWithDiagnostics( + tool: ToolInput, + ctx?: ResolveContext +): SandboxConfigResolution; + +declare function getSandboxConfigWithDiagnostics( + tools: readonly ToolInput[], + ctx?: ResolveContext +): SandboxConfigResolution; ``` -`resolveCatalogEntry` is singular by design, not an array-in/array-out call: -a caller composing and persisting requirements per tool (rather than as one -opaque merged blob) needs each result independently addressable and -independently attributable. A caller resolving several tools calls it once -per tool. `undefined` means no acceptable or resolvable identity/platform -match, never an empty policy (same distinction #779 makes; see -[§4.2](#42-entry-shape)). +A string input is shorthand for `{ invocationName: tool }`; it supplies no +package or version evidence and follows the same weak-identity option as an +object input. For example, name-only lookup under the current proposed opt-in +rule is: + +```ts +const ctx = { allowWeakIdentityFallback: true }; +const policy = getSandboxConfig("npm", ctx); +const combinedPolicy = getSandboxConfig(["git", "npm"], ctx); +const result = getSandboxConfigWithDiagnostics("npm", ctx); +const combinedResult = + getSandboxConfigWithDiagnostics(["git", "npm"], ctx); +``` + +Single-tool lookup is equivalent to a one-element array; its diagnostic +`inputIndex` is `0`. A caller retaining separate policies per tool can use +single-tool calls. A caller wanting one sandbox for several tools passes an +array. Both forms compose every eligible matching entry, not just the +strongest match, and the selected dependencies. + +`getSandboxConfig` returns the composed `SandboxPolicy` directly, not a wrapper +or a `ContainerConfig`. It is the exact type provided by the corresponding MXC +SDK, not a catalog-owned lookalike. Callers can pass an accepted policy directly +to that SDK without conversion or serialization. The `policy` field returned +by `getSandboxConfigWithDiagnostics` uses that same SDK type, with attribution +and warnings from the same resolution pass. Callers choose one operation; +retrieving diagnostics does not require a second lookup or process-global +"last result" state. + +Following #779, an unmatched input contributes no requirements while matched +inputs still contribute. Each input has a diagnostic record; an unmatched +input has an empty `matches` list and a warning. An empty input array or an +all-unmatched lookup produces no policy, not an empty policy: +`getSandboxConfig` returns `undefined`, while the diagnostics operation returns +a `SandboxConfigResolution` with `policy: undefined`. An empty array has no +per-input records. Unresolved required symbols in selected entries prevent a +policy from being returned and produce diagnostics; they are not grounds for +silently omitting a selected requirement to produce a partial policy. + +Multiple matching entries for one input are listed in `matches`, with a +warning identifying that input and the contributing entry IDs. Shared entries +remain attributed to every matching input even though their policy is +composed once. Dependency diagnostics retain each distinct +entry/revision/required-version-range combination, ordered by those fields; +repeated metadata does not mean repeated policy contribution. + +When architecture is omitted, diagnostics include a warning naming the +effective native system architecture and stating that the tool's architecture +was not verified. Architecture-neutral fallback is also identified. These +diagnostics describe selection; they do not attest to the installed tool's +architecture. The policy-only operation does not expose warnings or +attribution; consumers needing them use `getSandboxConfigWithDiagnostics`. + +Filesystem composition diagnostics report read-only requirements superseded +by read-write requirements and catalog denies removed to satisfy required +access. Each warning identifies the resolved paths, access classes, and +contributing entry IDs. A removed deny warning names the full removed scope +and explains that other grants may now apply throughout it, not only at the +overlap. Both APIs return the same composed policy; callers needing to review +these adjustments use `getSandboxConfigWithDiagnostics`. ### 5.2 Setup and inspection @@ -379,8 +647,9 @@ provenance, but not an unresolved or resolved policy body. A consumer that uses this API: 1. Decides whether automatic lookup is enabled at all. -2. Stores any accepted result per tool, keyed by `entryId`, `entryRevision`, - and `catalogRevision`, never as an unattributed merged policy blob. +2. When persisting an accepted policy, retains its `catalogRevision` and + contributing entry IDs/revisions from diagnostics, whether the policy + covers one tool or several. 3. Keeps catalog-derived requirements in a layer separate from its own user, learned, and invocation-specific policy. 4. Applies its own authorization, elevation, and restrictive-composition @@ -390,7 +659,8 @@ A consumer that uses this API: 6. Fails closed when a required entry cannot be realized on the current host/backend. It falls back to its own restrictive baseline and does not run uncontained. -7. Records matched identity, catalog/entry revision, warnings, and approval +7. Uses the diagnostics operation when attribution or audit is needed, and + records matched identities, catalog/entry revisions, warnings, and approval state in its own audit trail. The catalog libraries never write a consumer's policy store. A consumer's own @@ -406,40 +676,64 @@ validation, and publication workflow belong there. This specification remains in MXC while the proposed contract is reviewed. No catalog repository or package is created by this proposal. -MXC retains the existing `SandboxPolicy` contract. The catalog libraries -produce policy data conforming to that contract; they do not require an MXC -executor or execution library to perform lookup. A consumer that uses MXC -passes its final, authorized policy to an existing MXC SDK separately. -Catalog and library releases do not require an MXC SDK release or changes to -MXC repository governance. +MXC retains the existing `SandboxPolicy` contract and SDK types. Each catalog +library has a required dependency on its language's MXC SDK and constructs +that SDK's policy type. The dependency runs only from the catalog to MXC: +MXC neither references the catalog nor performs catalog lookup. A consumer +passes its final, authorized policy directly to the existing MXC SDK. +Standalone means separate repository, API, and release ownership, not absence +of SDK dependencies. SDK dependencies may bring native build or package +assets; catalog lookup itself does not invoke MXC sandbox execution. +Catalog and library releases using supported SDK contracts do not require +an MXC SDK release or changes to MXC repository governance. ### 6.1 Library distribution and consumption The initial library language coverage matches MXC's current first-party SDK languages, but the packages are owned and released by the catalog project: -| Language | Distribution | API form | +| Language | Distribution | MXC SDK dependency and policy type | |---|---|---| -| TypeScript / JavaScript | npm package | JavaScript library with TypeScript declarations | -| Rust | Cargo crate | Public Rust library API | -| C# / .NET | NuGet package | Managed library API | +| TypeScript / JavaScript | npm package | `SandboxPolicy` from `@microsoft/mxc-sdk` | +| Rust | Cargo crate | `mxc_sdk::SandboxPolicy` from `mxc-sdk` | +| C# / .NET | NuGet package | `Microsoft.Mxc.Sdk.SandboxPolicy` from `Microsoft.Mxc.Sdk` | Repository and package names remain to be selected. This language match does not require copying MXC's native-binding architecture or exposing sandbox execution operations. +Each library declares compatible MXC SDK versions. The catalog and caller must +resolve compatible SDK dependencies with the same policy type identity, +including crate source/version in Rust. Returned policies must use fields and +contract versions supported by that SDK; unsupported data must not be silently +dropped to fit its types. + A consumer: 1. Installs and pins the standalone library package for its language. Each package includes a reviewed default catalog revision for local use. -2. Calls `getCatalogInfo()` or `listCatalogEntries()` for inspection, or - `resolveCatalogEntry()` for one tool, using the corresponding language API. -3. Handles no match without widening its restrictive baseline. For a match, - it reviews the returned policy, identity, revisions, and warnings and - applies the consumer obligations in [§5.3](#53-consumer-obligations). +2. Calls `getCatalogInfo()` or `listCatalogEntries()` for inspection. + `getSandboxConfig()` returns a policy for one tool or an array; + `getSandboxConfigWithDiagnostics()` adds match attribution and warnings. +3. Handles policy absence without widening its restrictive baseline. It + reviews the composed policy and uses the diagnostics operation when it + needs contributing identities, revisions, and warnings, applying the + consumer obligations in [§5.3](#53-consumer-obligations). 4. Supplies its final policy to its chosen execution integration. The catalog library does not launch a sandbox. +The library API reference and repository/package READMEs must state: + +> Returns a candidate MXC `SandboxPolicy` combining the access requirements of +> all matching tools and their dependencies. Overlapping filesystem +> requirements use the least restrictive access needed to satisfy the combined +> requirements, including removal of conflicting catalog-provided denies. +> This is not authorization or a guarantee of workflow success. The caller +> decides whether to accept the requested access and must preserve its own +> user, enterprise, and device restrictions. Use +> `getSandboxConfigWithDiagnostics` to review contributing entries and access +> changes, including the full scope of any removed deny. + Lookup is local and does not download updates, contact a hosted service, or run the candidate tool. `ResolveContext.catalogRevision` selects an available local revision, not a network lookup; an explicitly requested revision that @@ -457,16 +751,26 @@ rewrite a consumer's previously accepted per-tool policies. ### 6.2 Cross-language consistency and support All three libraries use the same catalog format and shared conformance -fixtures. Given the same catalog revision, candidate, and explicit resolution -context, they must agree on matching, variant selection, dependency metadata, -resolved policy, warnings, and failure categories. Language-specific absence -and error types must preserve those distinctions. +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. The catalog digest format remains shared as +specified in [§10](#10-immutable-revisions). 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. Implementation sharing between languages is a separate -engineering decision, not a requirement to depend on MXC's engine. +engineering decision; catalog resolution does not move into MXC's engine. Supporting three languages includes maintaining parity, dependencies, documentation, and releases, not only writing the initial implementations. @@ -481,7 +785,7 @@ are intended to retire together when Learning Mode replaces this workflow. range(s), platform evidence, a minimized requirement set, test fixtures, and provenance. - CI validates schema conformance, exact `SandboxPolicy` version registration, - identity uniqueness, dependency closure and cycle-freedom, symbol validity, + 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. - A new entry or a requirement expansion requires one catalog-owner approval @@ -528,6 +832,13 @@ outcomes: 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 parent 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 @@ -548,13 +859,25 @@ 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. +In addition to schema validation, verify the stored revision content, +including shared symbol definitions, against its packaged expected digest +when loading it. A mismatch is an explicit library failure, not a no-match +result. Validated immutable data may be cached; each lookup need not rehash it. +The packaging format defines the digest input consistently across languages, +without requiring a custom JSON parser or canonicalization implementation. + +This lightweight check detects content that no longer matches the packaged +revision metadata. It does not independently authenticate the publisher or +protect against replacing both content and digest; authenticity remains a +property of the trusted package or artifact distribution channel. + ## 11. Backward compatibility - No change to `SandboxPolicy` or `ContainerConfig` schema. - No change to executor behavior. -- No change to the MXC SDK APIs or dependencies. Catalog lookup requires an - explicit call to a standalone library; existing MXC callers see no behavior - change. +- No change to the MXC SDK APIs and no catalog dependency added to MXC. + The catalog libraries depend on MXC SDKs, not the reverse. Catalog lookup + requires an explicit call; existing MXC callers see no behavior change. - Catalog schema and API compatibility are limited to the stopgap's support horizon. Retirement in favor of Learning Mode is an expected outcome, not a normal promotion milestone. @@ -564,23 +887,71 @@ have cached or recorded in an audit trail. **Resolver libraries (TypeScript/JavaScript, Rust, and C#/.NET)** - shared conformance fixtures produce equivalent results and failure - categories in all three languages -- single tool returns the expected entry; unknown tool returns `undefined`, - or the language-equivalent absence value, never an empty policy -- omitted context uses host platform/architecture, the installed catalog - revision, no caller symbol overrides, and no weak-identity fallback -- an unresolved required symbol returns `undefined`, never a partial policy -- identity match strength selection and weak-identity fallback behavior + 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 +- one-tool and one-element-array overloads produce equivalent policies and + diagnostics; the simple API returns the same policy as the diagnostic API +- multiple input tools compose all matching entries and dependencies into one + policy; repeated inputs or shared dependencies do not duplicate contributions +- 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 context uses 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 +- one input matching several entries composes all eligible matches, including + equal-strength matches; a stronger match does not suppress a weaker eligible + match, and diagnostics preserve all matching entries and identity evidence +- 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; additive matching does not bypass it +- invocation-name case variants match identically on all platforms without + changing command spelling or package-identity matching +- 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 - version-range mismatch produces a warning, not a refusal - exact-architecture variant precedes the platform-only variant; duplicate selectors are rejected; no matching variant produces `undefined` +- 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 variant falls back to the platform's neutral variant; + a different architecture's variant is never used as a fallback +- successful host-derived selection and neutral fallback produce the + diagnostics specified in [§5.1](#51-runtime-lookup); host-architecture + detection failure produces a library error, not a guessed match - dependency chain resolution, including cycles (terminate, no duplication) - dependency `versionRange` is returned as unevaluated metadata and never used for v1 resolver matching -- restricted composition rules ([§4.5](#45-dependencies-and-composition)): - same-class path de-duplication; cross-class path overlap, mixed policy - versions, network fields, and other unsupported composed fields are rejected - at validation time rather than resolved +- 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 +- 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 +- mixed policy versions, network fields, and other unsupported composed fields + remain rejected, including incompatible entries selected together only at + lookup time - symbol resolution on Windows, Linux, and macOS **Data (CI)** @@ -593,16 +964,26 @@ have cached or recorded in an audit trail. **Integration** -- each package installs and performs lookup without an MXC executor or - execution library; lookup requires no network access +- each package installs with its declared MXC SDK dependency and performs + lookup without invoking sandbox execution; lookup requires no network access +- compiled consumer examples in all three languages pass both the direct + result and the diagnostic result's policy to existing MXC SDK APIs without + casts, adapters, or serialization; the same SDK policy type is used throughout - the bundled catalog revision matches `getCatalogInfo()`; selecting an unavailable revision fails explicitly, without falling back to another revision +- changing stored catalog content without updating its expected digest fails + on load even when the changed content still passes schema validation - a package 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 deny still prevents access even when an overlapping + catalog-provided deny was removed during floor composition ## 13. Open questions @@ -614,8 +995,8 @@ Recommended answers are proposals for review, not decisions. | Is invocation-name-only identity accepted automatically, or does it require explicit consumer opt-in? | Treat it as a fallback requiring explicit opt-in (`allowWeakIdentityFallback`), not the default. | | What happens on a detected tool-version mismatch: `undefined`, or a warning-bearing result the consumer may still use? | Return the resolved result with a warning; refusing outright removes information the consumer needs to decide for itself. | | Are private or enterprise catalog overlays in scope, and if so with what precedence? | Defer until the shared catalog contract and its API are stable; define precedence explicitly before any library implementation adds overlay support. | -| Should the first contract version's composition vocabulary expand beyond [§4.5](#45-dependencies-and-composition) before implementation? | No. Start with conflict-rejecting filesystem composition and expand only with an explicit, reviewed rule per field. | -| Who owns catalog schema, data, and library API review? | Assign catalog, library, and security reviewers in the dedicated repository; no MXC SDK integration is proposed. | +| Should the first contract version's composition vocabulary expand beyond [§4.5](#45-dependencies-and-composition) before implementation? | No. Start with least-restrictive filesystem floor composition and expand only with an explicit, reviewed rule per field. | +| Who owns catalog schema, data, and library API review? | Assign catalog, library, and security reviewers in the dedicated repository; MXC SDKs remain unaware of the catalog. | | Should the libraries share a resolver implementation or implement the contract independently? | Choose based on dependency footprint and maintenance cost, with shared conformance fixtures required either way. | ## 14. Related work From dcff41d2030956d37606df2a007117f3844d613c Mon Sep 17 00:00:00 2001 From: Chaz Gordish Date: Thu, 1 Oct 2026 10:54:26 -0700 Subject: [PATCH 08/12] docs: rename catalog policy resolution APIs Use resolveSandboxPolicy and resolveSandboxPolicyWithDiagnostics throughout the spec, retain the original proposal's name as historical context, and make runtime and inspection declarations valid exported TypeScript signatures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/mxc-policy-store.md | 44 ++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/mxc-policy-store.md b/docs/mxc-policy-store.md index f76e4bfbb..e2298dc71 100644 --- a/docs/mxc-policy-store.md +++ b/docs/mxc-policy-store.md @@ -91,8 +91,8 @@ Defaults and omission behavior are: symbol is not resolvable. - Omitted `packageUrl` or `detectedVersion` supplies no matching evidence. The resolver does not fabricate either value. -- If no policy can be resolved, `getSandboxConfig` returns `undefined`. - `getSandboxConfigWithDiagnostics` instead returns a result whose `policy` is +- If no policy can be resolved, `resolveSandboxPolicy` returns `undefined`. + `resolveSandboxPolicyWithDiagnostics` instead returns a result whose `policy` is `undefined`, preserving the diagnostics. The consumer's restrictive baseline remains unchanged. @@ -122,7 +122,7 @@ Everything else is a consumer decision: sandbox creation. A catalog lookup can only ever narrow what a consumer still has to decide for -itself. `getSandboxConfig` returns a candidate composed requirement or +itself. `resolveSandboxPolicy` returns a candidate composed requirement 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 @@ -138,7 +138,7 @@ required to grant. | Strongest satisfied identity predicate describes a match | All eligible matching entries contribute; identity evidence is retained without stronger matches suppressing weaker ones ([§4.3](#43-identity)) | | One `sandboxPolicy` per entry; `when.platform` only conditions dependencies | One complete `SandboxPolicy` per platform variant; a variant cannot name a containment backend ([§4.4](#44-platform-variants)) | | `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 | `getSandboxConfig` accepts one tool or an array and returns one policy; `getSandboxConfigWithDiagnostics` adds attribution, with catalog inspection kept separate ([§5](#5-api-surface)) | +| `getSandboxConfigForTool(tools: string[])` returns one composed policy | Replaced by `resolveSandboxPolicy`, accepting one tool or an array and returning one policy; `resolveSandboxPolicyWithDiagnostics` adds attribution, with catalog inspection kept separate ([§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, @@ -523,22 +523,22 @@ interface SandboxConfigResolution { }; } -declare function getSandboxConfig( +export declare function resolveSandboxPolicy( tool: ToolInput, ctx?: ResolveContext ): SandboxPolicy | undefined; -declare function getSandboxConfig( +export declare function resolveSandboxPolicy( tools: readonly ToolInput[], ctx?: ResolveContext ): SandboxPolicy | undefined; -declare function getSandboxConfigWithDiagnostics( +export declare function resolveSandboxPolicyWithDiagnostics( tool: ToolInput, ctx?: ResolveContext ): SandboxConfigResolution; -declare function getSandboxConfigWithDiagnostics( +export declare function resolveSandboxPolicyWithDiagnostics( tools: readonly ToolInput[], ctx?: ResolveContext ): SandboxConfigResolution; @@ -551,11 +551,11 @@ rule is: ```ts const ctx = { allowWeakIdentityFallback: true }; -const policy = getSandboxConfig("npm", ctx); -const combinedPolicy = getSandboxConfig(["git", "npm"], ctx); -const result = getSandboxConfigWithDiagnostics("npm", ctx); +const policy = resolveSandboxPolicy("npm", ctx); +const combinedPolicy = resolveSandboxPolicy(["git", "npm"], ctx); +const result = resolveSandboxPolicyWithDiagnostics("npm", ctx); const combinedResult = - getSandboxConfigWithDiagnostics(["git", "npm"], ctx); + resolveSandboxPolicyWithDiagnostics(["git", "npm"], ctx); ``` Single-tool lookup is equivalent to a one-element array; its diagnostic @@ -564,11 +564,11 @@ single-tool calls. A caller wanting one sandbox for several tools passes an array. Both forms compose every eligible matching entry, not just the strongest match, and the selected dependencies. -`getSandboxConfig` returns the composed `SandboxPolicy` directly, not a wrapper +`resolveSandboxPolicy` returns the composed `SandboxPolicy` directly, not a wrapper or a `ContainerConfig`. It is the exact type provided by the corresponding MXC SDK, not a catalog-owned lookalike. Callers can pass an accepted policy directly to that SDK without conversion or serialization. The `policy` field returned -by `getSandboxConfigWithDiagnostics` uses that same SDK type, with attribution +by `resolveSandboxPolicyWithDiagnostics` uses that same SDK type, with attribution and warnings from the same resolution pass. Callers choose one operation; retrieving diagnostics does not require a second lookup or process-global "last result" state. @@ -577,7 +577,7 @@ Following #779, an unmatched input contributes no requirements while matched inputs still contribute. Each input has a diagnostic record; an unmatched input has an empty `matches` list and a warning. An empty input array or an all-unmatched lookup produces no policy, not an empty policy: -`getSandboxConfig` returns `undefined`, while the diagnostics operation returns +`resolveSandboxPolicy` returns `undefined`, while the diagnostics operation returns a `SandboxConfigResolution` with `policy: undefined`. An empty array has no per-input records. Unresolved required symbols in selected entries prevent a policy from being returned and produce diagnostics; they are not grounds for @@ -595,7 +595,7 @@ effective native system architecture and stating that the tool's architecture was not verified. Architecture-neutral fallback is also identified. These diagnostics describe selection; they do not attest to the installed tool's architecture. The policy-only operation does not expose warnings or -attribution; consumers needing them use `getSandboxConfigWithDiagnostics`. +attribution; consumers needing them use `resolveSandboxPolicyWithDiagnostics`. Filesystem composition diagnostics report read-only requirements superseded by read-write requirements and catalog denies removed to satisfy required @@ -603,7 +603,7 @@ access. Each warning identifies the resolved paths, access classes, and contributing entry IDs. A removed deny warning names the full removed scope and explains that other grants may now apply throughout it, not only at the overlap. Both APIs return the same composed policy; callers needing to review -these adjustments use `getSandboxConfigWithDiagnostics`. +these adjustments use `resolveSandboxPolicyWithDiagnostics`. ### 5.2 Setup and inspection @@ -633,8 +633,8 @@ interface CatalogEntryMetadata { }; } -listCatalogEntries(): CatalogEntryMetadata[]; -getCatalogInfo(): { catalogSchemaVersion: string; catalogRevision: string }; +export declare function listCatalogEntries(): CatalogEntryMetadata[]; +export declare function getCatalogInfo(): { catalogSchemaVersion: string; catalogRevision: string }; ``` This supports setup UI, catalog browsing, and update decisions without paying @@ -713,8 +713,8 @@ A consumer: 1. Installs and pins the standalone library package for its language. Each package includes a reviewed default catalog revision for local use. 2. Calls `getCatalogInfo()` or `listCatalogEntries()` for inspection. - `getSandboxConfig()` returns a policy for one tool or an array; - `getSandboxConfigWithDiagnostics()` adds match attribution and warnings. + `resolveSandboxPolicy()` returns a policy for one tool or an array; + `resolveSandboxPolicyWithDiagnostics()` adds match attribution and warnings. 3. Handles policy absence without widening its restrictive baseline. It reviews the composed policy and uses the diagnostics operation when it needs contributing identities, revisions, and warnings, applying the @@ -731,7 +731,7 @@ The library API reference and repository/package READMEs must state: > This is not authorization or a guarantee of workflow success. The caller > decides whether to accept the requested access and must preserve its own > user, enterprise, and device restrictions. Use -> `getSandboxConfigWithDiagnostics` to review contributing entries and access +> `resolveSandboxPolicyWithDiagnostics` to review contributing entries and access > changes, including the full scope of any removed deny. Lookup is local and does not download updates, contact a hosted service, or From 539fa50013aa3e668ee4c4cb129efef7ea26ddaa Mon Sep 17 00:00:00 2001 From: Chaz Gordish Date: Thu, 1 Oct 2026 11:52:11 -0700 Subject: [PATCH 09/12] docs: clarify catalog warning and failure mappings Make the opening access and safety caveat explicit, map catalog failures to existing MXC error codes with optional reasons, and add the corresponding conformance check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/mxc-policy-store.md | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/docs/mxc-policy-store.md b/docs/mxc-policy-store.md index e2298dc71..61fc1b88a 100644 --- a/docs/mxc-policy-store.md +++ b/docs/mxc-policy-store.md @@ -3,11 +3,11 @@ **Status:** Proposed public preview catalog. This is not an approved, shipped, or implemented catalog. -**IMPORTANT NOTE:** This is a time-limited bridge, not a long-term supported -Microsoft product. Applying a published floor does not guarantee that a tool's -end-to-end workflow will work under process containment. The catalog and its -dedicated repository are expected to be retired when Learning Mode provides -the replacement workflow. +**IMPORTANT NOTE:** This catalog and its repository are a temporary bridge to +Learning Mode, not a long-term supported product. 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 take +precedence. --- @@ -479,6 +479,23 @@ when the code alone is too broad. Callers need not branch on it; an absent or unrecognized reason retains the same handling as the primary code. A reason must not duplicate a distinction already expressed by an existing MXC code. +The following primary-code mappings apply across all language bindings. +When `details.reason` is supplied for these failures, it uses the listed value; +callers may ignore it. + +| Failure | MXC error code | Optional `details.reason` | +|---|---|---| +| Invalid tool input or resolution context | `malformed_request` | `invalid_context` | +| Invalid catalog data, including invalid dependency references or cycles | `policy_validation` | `invalid_catalog` | +| Unsupported composition, including mixed policy versions or fields without a composition rule | `policy_validation` | `composition_conflict` | +| Unsupported or undetectable host platform or architecture | `unsupported_containment` | `unsupported_host` | +| Catalog content cannot be read or fails its integrity check | `backend_error` | `integrity` | +| Explicitly requested catalog revision is not installed | `backend_error` | `revision_unavailable` | + +Filesystem overlaps handled by [§4.5](#45-dependencies-and-composition) are not +composition failures. Ordinary no-match results remain policy absence, not an +error from this table. + ```ts import type { SandboxPolicy } from "@microsoft/mxc-sdk"; @@ -890,6 +907,8 @@ property of the trusted package or artifact distribution channel. 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 primary-code mappings in [§5.1](#51-runtime-lookup); + any supplied reason uses its listed value, and callers can handle the code alone - one-tool and one-element-array overloads produce equivalent policies and diagnostics; the simple API returns the same policy as the diagnostic API - multiple input tools compose all matching entries and dependencies into one From 5a2c52cdbfbd810d4f350db14d91f3244edb34a9 Mon Sep 17 00:00:00 2001 From: Chaz Gordish Date: Mon, 5 Oct 2026 10:40:54 -0700 Subject: [PATCH 10/12] docs: apply design review outcomes to Policy Store spec Ship as MXC SDK APIs with bundled V1 data, add intents and add-only platform/version overlays (newIntents vs intentAdditions), per-pair resolution statuses, default-only dependencies, and catalog filesystem and egress deny removal on conflict. Linux invocation names match exactly. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 2 +- docs/mxc-policy-store.md | 981 ++++++++++++++++++++++++++------------- 2 files changed, 656 insertions(+), 327 deletions(-) diff --git a/README.md b/README.md index 243ffe80b..309f1254c 100644 --- a/README.md +++ b/README.md @@ -316,7 +316,7 @@ 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.md](docs/mxc-policy-store.md) | Proposed public preview known-tool policy catalog and standalone libraries | +| [docs/mxc-policy-store.md](docs/mxc-policy-store.md) | MXC SDK Policy Store APIs and bundled best-effort policy data | | [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.md b/docs/mxc-policy-store.md index 61fc1b88a..3ae1fb15a 100644 --- a/docs/mxc-policy-store.md +++ b/docs/mxc-policy-store.md @@ -1,13 +1,14 @@ # Feature Spec: Known-tool Policy Floors -**Status:** Proposed public preview catalog. This is not an approved, shipped, or -implemented catalog. +**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:** This catalog and its repository are a temporary bridge to -Learning Mode, not a long-term supported product. Floors may broaden file +**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 take -precedence. +clients must review that access; their settings and enterprise policies can +further constrain or override the recommendation. --- @@ -18,18 +19,18 @@ needed for their workflow. This makes the first-run experience for process containment poor and reduces adoption before developers can identify the missing policy. -The near-term mitigation is a public, reviewable set of known per-tool policy -floors. Consumers can apply a candidate floor instead of starting from no tool -knowledge, and contributors can iterate on the data as failures are found. A -floor only describes a known minimum requirement. It does not prove that every -process, dependency, credential, service, or network interaction in an -end-to-end workflow is covered. +The solution is a reviewed set of policy floors bundled with MXC and +exposed through its SDKs. Consumers can apply a best-effort baseline instead of +starting from no tool knowledge, and contributors can improve the data as +failures are found. A floor describes what is believed to be needed for +representative cases. It does not prove that every process, dependency, +credential, service, or network interaction in an end-to-end workflow is covered. [#779](https://github.com/microsoft/mxc/pull/779) proposed the initial config -floor data model and SDK resolver. This document narrows that proposal into a -temporary catalog contract with explicit identity, platform, dependency, -revision, and inspection behavior. It reuses #779's model where possible and -calls out differences directly. +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. This document does not restate general MXC sandboxing concepts already covered by [`docs/sandbox-policy/0.8.0/policy.md`](sandbox-policy/0.8.0/policy.md) or @@ -51,27 +52,21 @@ by [`docs/sandbox-policy/0.8.0/policy.md`](sandbox-policy/0.8.0/policy.md) or ### MXC feature impact and defaults -This is a standalone catalog and library proposal. Following the feature-impact -checklist in [`docs/authoring-a-new-feature.md`](authoring-a-new-feature.md): +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. Catalog entries embed an existing, registered - `SandboxPolicy`. +- **Policy changes:** None. Entries use MXC's policy schema. - **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:** None. This proposal does not add catalog lookup, types, - or resolver code to the MXC SDKs. -- **Standalone libraries:** The dedicated catalog repository provides its own - library API for TypeScript/JavaScript, Rust, and C#/.NET, each depending on - the corresponding MXC SDK and returning its existing `SandboxPolicy` type. - MXC does not depend on the catalog. See +- **MXC SDK changes:** Add policy-resolution and inspection APIs to the existing + TypeScript/JavaScript, Rust, and C#/.NET SDKs, returning the SDK's policy type. +- **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). -The proposed **public preview** designation describes the catalog's support -status. It does not add an MXC schema feature, activate the -`--experimental` runtime gate, or change executor behavior. - Defaults and omission behavior are: - Existing callers do not perform catalog lookup automatically. A consumer @@ -89,8 +84,12 @@ Defaults and omission behavior are: symbols as specified in [§4.2](#42-entry-shape). It does not invent a project root or an installation path. A selected entry with an unresolved required symbol is not resolvable. -- Omitted `packageUrl` or `detectedVersion` supplies no matching evidence. The - resolver does not fabricate either value. +- The resolver does not fabricate an omitted `packageUrl` or `detectedVersion`. +- Omitted `ToolCandidate.detectedVersion` selects the unversioned default, + without a version warning. +- Omitted `ToolCandidate.intent` selects the base plus all intents of the + effective version policy. An unsupported intent contributes no policy and + produces an `intent_unsupported` warning. - If no policy can be resolved, `resolveSandboxPolicy` returns `undefined`. `resolveSandboxPolicyWithDiagnostics` instead returns a result whose `policy` is `undefined`, preserving the diagnostics. The consumer's restrictive baseline @@ -98,17 +97,15 @@ Defaults and omission behavior are: ## 2. Ownership boundary -The proposed dedicated catalog project owns an integrity-validated, versioned, -read-only data set of known-tool sandbox requirements, its resolver libraries, -and their public APIs. MXC continues to own the existing `SandboxPolicy` -contract, but not the catalog entries, libraries, repository, or publication -lifecycle. +MXC owns the reviewed, versioned, read-only catalog, its resolution and +inspection APIs, and their SDK publication lifecycle in `microsoft/mxc`. +The policy contract and returned SDK type remain MXC-owned as well. -The catalog states a candidate minimum that a tool needs. It does not grant +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 composition combines 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. +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: @@ -134,9 +131,9 @@ required to grant. | #779 (config floors) | This document (policy store) | |---|---| -| One `schemaVersion` for the whole table | Four separate version dimensions: `catalogSchemaVersion`, `catalogRevision`, per-entry `entryRevision`, and per-variant `sandboxPolicy.version` ([§4.1](#41-versions)) | -| Strongest satisfied identity predicate describes a match | All eligible matching entries contribute; identity evidence is retained without stronger matches suppressing weaker ones ([§4.3](#43-identity)) | -| One `sandboxPolicy` per entry; `when.platform` only conditions dependencies | One complete `SandboxPolicy` per platform variant; a variant cannot name a containment backend ([§4.4](#44-platform-variants)) | +| One `schemaVersion` for the whole table | Four separate version dimensions: `catalogSchemaVersion`, `catalogRevision`, per-entry `entryRevision`, and `default.sandboxPolicy.version` ([§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 `resolveSandboxPolicy`, accepting one tool or an array and returning one policy; `resolveSandboxPolicyWithDiagnostics` adds attribution, with catalog inspection kept separate ([§5](#5-api-surface)) | | No revision/publication model | Immutable published catalog revisions; corrections publish a new revision ([§10](#10-immutable-revisions)) | @@ -155,38 +152,101 @@ are stated here rather than implied by that reference. | `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. | -| `sandboxPolicy.version` | The exact registered `SandboxPolicy` contract version used by one platform variant. | +| `default.sandboxPolicy.version` | Policy-schema version inherited by all additions for the entry. | These identifiers serve separate purposes and do not advance in lockstep. Registering a new `SandboxPolicy` contract does not change existing catalog -data and therefore does not require a new catalog revision. Migrating a -platform variant to that contract changes the entry's content, so publication +data and therefore does not require a new catalog revision. Migrating an +entry to that contract changes the entry's content, so publication of that migration must increment both `entryRevision` and `catalogRevision`. A catalog revision may still change without incrementing unaffected entries. -Tool version constraints (`versionRange`, below) are a fifth, orthogonal axis. -They describe which builds of a tool an entry was observed against, not -anything about the catalog. +Tool versions and `versionRange` values are distinct from catalog revisions. + +Entries use MXC's policy schema and remain compatible within MXC 1.x; +breaking policy-schema changes require 2.x. The SDK selects the exact +configuration contract as described in [versioning.md](versioning.md). ### 4.2 Entry shape ```json { - "entryId": "tool:npm", + "entryId": "tool:git", "entryRevision": 3, - "displayName": "npm / npx", + "displayName": "Git", + "versionScheme": "intdot", "identity": [ - { "kind": "purl", "value": "pkg:npm/npm", "versionRange": ">=10 <12" }, - { "kind": "invocation-name", "names": ["npm", "npm.cmd", "npx", "npx.cmd"] } + { "kind": "purl", "value": "pkg:generic/git" }, + { "kind": "invocation-name", "names": ["git", "git.exe"] } ], + "default": { + "sandboxPolicy": { + "version": "0.9.0-alpha", + "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" }, - "dependencies": [{ "entryId": "tool:node", "versionRange": ">=22" }], - "sandboxPolicy": { - "version": "0.9.0-alpha", - "filesystem": { - "readonlyPaths": ["${npm_prefix}"], - "readwritePaths": ["${project_root}", "${npm_cache}"] + "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 }] } + ] + } + } + } } } } @@ -195,22 +255,68 @@ anything about the catalog. } ``` +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 a minimal base `sandboxPolicy` and intent additions. +Unversioned means no tool-version selector; the policy-schema version remains +separate. + +`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 `requires`/ - dependency edges may reference. +- `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 selected variant means the tool is - unsupported on that platform, not that it needs an empty policy, and not - `undefined` conflated with "requires nothing" (see [#779, "Defaults and - omission"](https://github.com/microsoft/mxc/pull/779)). + [§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. -- An embedded `sandboxPolicy` is validated against the real `SandboxPolicy` - schema for its declared `version`. The catalog schema does not duplicate - that validation. +- Embedded policies use MXC's policy schema, not a separate catalog policy + vocabulary. [§13](#13-open-questions) tracks the concrete validator selection. Symbol definitions are shared across entries and versioned with the selected catalog revision. Each definition describes the symbol's permitted sources @@ -227,6 +333,10 @@ shared symbol registry can include: "linux": "${user_home}/.npm", "macos": "${user_home}/.npm" } + }, + "programData": { + "source": "host", + "description": "Windows common application-data directory." } } } @@ -235,8 +345,7 @@ shared symbol registry can include: 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 -covered by the catalog revision's integrity validation and cannot change -behind a pinned revision. +embedded with the entries and cannot change behind a pinned catalog revision. For symbols required by selected entries and dependencies, precedence is: @@ -257,6 +366,13 @@ of each discovered or defaulted symbol through `diagnostics.warnings`. ### 4.3 Identity +Lookup uses tool identity, an optional detected version, and optional +tool-defined intent. Package URL +provides strong identity and invocation name is the opt-in fallback. Raw +command lines and calling-application identity are not lookup keys; the caller +maps operations to intents and applies its own restrictions. Platform and +architecture remain resolution context. + `identity` describes the predicates a candidate can satisfy for an entry, using the layering #779 §3.1 establishes (invocation name vs. launcher artifact vs. executing image; falsifiable-against-a-local-artifact as the admission test @@ -267,43 +383,81 @@ not prove that the installed tool belongs to that package; the caller is responsible for verifying that association. The library does not inspect the tool to verify it. -Invocation-name matching is locale-independent and case-insensitive on every -platform. This affects catalog lookup only: it does not change the caller's -command, the OS's executable lookup rules, or package-identity matching. - -Matching is additive across entries. For each input tool, the resolver collects -every entry with a satisfied, eligible identity predicate and an applicable -platform/architecture variant. It does not choose a single winning entry: - -- A package-identity match does not suppress another entry's eligible - invocation-name match. Equal-strength matches to different entries also - contribute; they are not ambiguity errors. -- Identity strength describes the matching evidence, not precedence between - entries. Diagnostics retain all satisfied identity predicates for each - contributing entry. -- Multiple predicates matching the same entry do not add its policy multiple - times. Entries shared across input tools or dependency chains likewise - contribute once, while diagnostics preserve the per-input matches. -- Multiple matching entries for one input produce a diagnostic warning, not - a refusal. Their policies must still satisfy the composition rules in - [§4.5](#45-dependencies-and-composition). -- Diagnostic tool records follow input order, matches are ordered by - `entryId`, and matched predicates follow their declaration order within the - entry. Catalog file order does not select or exclude a match. - -The existing matching qualifications remain: - -- A version range on an identity predicate is advisory matching evidence, not - a gate. A detected mismatch returns a diagnostic alongside the resolved - policy rather than silently degrading precision, and the consumer decides - what to do with the mismatch. -- Invocation-name-only identity is the always-available fallback, not the - default outcome. Whether a consumer accepts an invocation-name-only match - automatically, or requires opt-in, is unresolved. See - [§13](#13-open-questions). Under the current proposed default, an entry - matched only by invocation name participates when - `allowWeakIdentityFallback` is `true`. Compose-all-matches does not bypass - that option. +Invocation-name matching is locale-independent and case-insensitive on Windows +and macOS, and exact on Linux. This does not change the caller's command or +package-identity matching. + +For each input tool, consider entries with an eligible identity predicate and +their applicable platform additions. Rank matches in this order: + +1. Package URL match over invocation-name-only match. +2. A requested intent declared in the default or applicable overlays over no + matching intent declaration. +3. Exact architecture over architecture-neutral additions or the common default. + +Select the unique highest-ranked match. Two distinct matches tied after all +three comparisons produce an error, not a union or a file-order tie-break. +Multiple predicates satisfied within the same entry count as one identity +match, ranked by its strongest satisfied predicate. + +After selecting the entry, resolve its version and then its intent as below. +Version ranges do not choose a different tool entry. +Failure for that pair does not retry another entry or a wildcard entry. +Composition includes only contributing pairs in an array request. + +Composition applies across tools in an array request, not across competing +identity matches for one tool. Diagnostics follow input order and identify the +single selected entry, satisfied identity predicates, and selected intents. + +Invocation-name-only matching requires explicit opt-in +(`allowWeakIdentityFallback: true`). Intent selection does not bypass that +option. + +`versionRange` uses the Package-URL project's +[VERS syntax](https://github.com/package-url/vers-spec/blob/797c842a4afebf258e6710a68cd60306afc36708/docs/specification/standard/Clause-5-VERS-Specification.md): +`vers:/`, for example `vers:npm/>=10.0.0|<12.0.0`. +V1 supports these [version types](https://github.com/package-url/vers-spec/blob/797c842a4afebf258e6710a68cd60306afc36708/docs/types/vers-types.md): + +| VERS type | Version parsing and comparison | +|---|---| +| `npm` | node-semver version rules, as referenced by the VERS npm definition | +| `semver` | Semantic Versioning 2.0.0 | +| `pypi` | PEP 440 | +| `nuget` | NuGet version normalization and comparison | +| `intdot` | VERS dotted-integer comparison for numeric tool versions such as Git `2.40` | + +VERS defines the range syntax and interval evaluation; the named type defines +version parsing, normalization, and ordering, including prereleases and +accepted prefixes. Do not apply npm's native range syntax to other types or +strip version prefixes independently of the named rules. `generic` is not +supported while its upstream comparison algorithm is unspecified. + +Catalog authoring/build validation rejects malformed VERS strings, invalid +constraints, and unsupported types as invalid catalog data. + +For each requested tool/intent pair, parse `detectedVersion` under the selected +entry's `versionScheme`. Use only that scheme's specified parsing and +normalization; do not repair rejected input, try other schemes, or perform +fuzzy matching. + +| Version input | Effective policy data | Version status / warning | +|---|---|---| +| Omitted | Default plus platform additions | `matched_default`; no version warning | +| Valid, inside exactly one range | Default plus platform and selected version additions | `matched_version`; record the selected range | +| Valid, in no range | Default plus platform additions | `version_out_of_range` warning | +| Unparseable under the entry's scheme | None for this pair | `version_unparseable` warning | + +An out-of-range version never selects the nearest, highest, or broadest variant. +An unparseable version contributes nothing, while other requested pairs still +resolve. After version selection, a named intent must exist in the effective +policy. Otherwise emit `intent_unsupported` and contribute nothing for that +pair, not the base or an all-intents fallback. An out-of-range version requesting +a newer-only intent emits both warnings and contributes nothing. With no +intent, combine the effective base with all of its effective intents. + +No catalog match yields `tool_unmatched`, no contribution, and no wildcard +fallback. Diagnostics preserve input order. These per-pair outcomes are not +whole-request failures. ### 4.4 Platform variants @@ -317,14 +471,17 @@ interface PlatformVariantSelector { } ``` -A platform variant is a complete requirement statement: one full -`SandboxPolicy`, not a patch applied to a base policy, plus any -platform-specific dependencies. Variants are never merged. Architecture is a -catalog selector, not a field added to the embedded `SandboxPolicy`. Omitting +A platform variant contains only additions to the common default, using the +same `policyAdditions`, `dependencies`, `intentAdditions`, and `newIntents` +fields as a version overlay. It never removes, narrows, or replaces default +requirements. Select at most one platform/architecture overlay, then combine +its additions with the default and the selected version overlay. +Architecture is a catalog selector, not a field added to `SandboxPolicy`. Omitting `when.architecture` makes a catalog variant architecture-neutral; omitting the caller's `ResolveContext.architecture` instead requests the host default. -Selection first filters by platform, then uses the following precedence: +Selection first filters by platform. After identity and intent specificity +([§4.3](#43-identity)), architecture selection uses the following precedence: | Caller context | Preferred variant | Fallback | |---|---|---| @@ -341,8 +498,8 @@ return a result when the effective architecture has an exact match. Catalog validation rejects duplicate exact selectors and more than one architecture-neutral variant for the same platform. If neither an exact nor -architecture-neutral variant exists, that entry contributes no match, not an -empty policy or a variant for a different architecture. A failure to determine +architecture-neutral variant exists, retain the common default without platform +additions; never select another architecture's overlay. A failure to determine the native system architecture when it is needed is a library error, not a guessed selection. @@ -363,34 +520,43 @@ requirement can be realized on that host. ### 4.5 Dependencies and composition -Dependencies reference another entry's `entryId` and live inside the platform -variant when platform-specific. An optional `versionRange` records which -dependency versions supplied the reviewed evidence. The v1 resolver has no -dependency inventory, so it does not evaluate that range or use it for -matching. It returns the range as unevaluated metadata for consumer inspection. -Catalog validation checks only that the range is syntactically valid. +Dependencies reference another entry's `entryId` and can belong to the base or +to an intent, in the default or a selected overlay. Include base dependencies +and only the selected intent dependencies. A dependency contributes only its +unversioned default base plus its applicable platform overlay's base +additions; it never selects a version overlay or includes intents. This +differs from a requested tool with no intent, which includes all effective +intents. A reference may name dependency intents, for example +`{ "entryId": "tool:ssh", "intents": ["connect"] }`; those intents, including +their applicable platform `intentAdditions`, are then added. Catalog validation +rejects a named dependency intent that is missing from any materialized +platform combination where the reference applies. An optional dependency +`versionRange` uses the same VERS syntax and authoring/build validation; +a range is not a detected version and does not select a version overlay. Resolution is otherwise transitive, cycle-rejecting, and deterministic, and the diagnostics API returns the resolved dependency metadata alongside the policy. -The same composition rules apply to all entries matched by one tool, entries -matched by different tools in an array, and their transitive dependencies. -Each selected entry contributes its policy once, even when reached through -multiple inputs or dependency edges. Repeated contribution is de-duplicated -by entry ID within the selected catalog revision, not by discarding match -attribution. A shared `ResolveContext` applies to the whole lookup. +Compose each selected base with its selected intent additions, then combine +the results for different requested tools and their transitive dependencies. +De-duplicate contributions by catalog revision, entry ID, selected version +range (or default), platform variant, and intent name, not by entry ID alone. +Repeated identical components contribute once; different requested versions +or intents retain their own additions. +Preserve per-input attribution. A shared `ResolveContext` applies to the whole +lookup; each tool candidate carries its own intent. Following #779's floor semantics, filesystem composition preserves the access required by all selected tools rather than intersecting their requirements. An overlapping read-only requirement must not suppress another tool's needed write access, and a catalog-provided deny must not block a required read or write path. This is not a rule for merging consumer authorization policy. -For the first contract version, cross-entry composition is limited to the exact +Filesystem composition uses the exact `filesystem.deniedPaths`, `filesystem.readonlyPaths`, and `filesystem.readwritePaths` fields: -1. Every policy in the selected entries and their dependency closure must - declare the same `sandboxPolicy.version`. +1. Every base policy in the selected entries and their dependency closure must + declare the same `sandboxPolicy.version`; intent additions inherit it. 2. At lookup time, resolve all required symbols and normalize paths using the selected platform's path rules before comparing equal or ancestor/descendant paths. Combine the selected entries and dependencies, @@ -444,24 +610,55 @@ apply these rules after substitution and normalization at lookup time. An overlap covered by these rules is not a composition error; missing required symbols or unsupported composed fields retain their existing failure behavior. -The v1 contract does not compose `network`. In particular, it defines no merge -for `network.egress.default`, `network.egress.allow`, -`network.egress.deny`, `network.ingress.default`, or -`network.ingress.hostLoopback`. Composition rejects a selected set of entries -where policies from more than one entry would require composing any `network` -field. The same rejection applies to timeout, clipboard, lifecycle, UI, proxy, -and every other policy field without an explicit cross-entry rule. A lookup -resolving to only one entry without dependencies may still use -catalog-supported policy fields because no cross-entry merge occurs. +Network requirements also combine across selected tools, intents, and +dependencies. A tool that needs no network contributes no network access; it +does not veto access required by another requested tool. For example, Git's +`local` intent alone needs no network, while a request combining it with a +tool needing HTTPS access includes that tool's HTTPS requirement. + +When only one selected component requires network, preserve its supported +network requirements; components with no grants do not force an additional +network merge. For multiple scoped outbound requirements in v1, retain +`network.egress.default: "deny"` and union the selected +`network.egress.allow` and catalog `network.egress.deny` rules, +de-duplicating identical rules. +Preserve each whole rule's destination, exclusions, protocol, and port +relationships; never form a cross-product of destinations and ports. Missing +network fields or deny-by-default contribute no grants, not a restriction on +another component's required grants. Do not introduce unrestricted outbound +access or include unselected version or intent additions. An omitted intent +selects all intents in the effective version policy; an unsupported intent +contributes no access. + +Catalog egress denies follow the filesystem deny rule. Remove each catalog +deny rule that overlaps any required allow rule, meaning their destination +CIDRs intersect after `except` exclusions and their protocol/port selectors +intersect. Remove the entire deny rule, not an invented exception within it. +Retain non-overlapping deny rules. The same rule applies within one entry and +across entries, and a conflicting deny never fails the request. +Diagnostics identify the removed rule, its full destination and port scope, +and the contributing entries. + +This combined access belongs to the shared sandbox, not to isolated +permissions per tool. Caller-owned network denies and other user, enterprise, +device, or backend restrictions are never inputs to this composition and are +never removed by it. Other network configuration, including allow-by-default, +non-default ingress, and proxy configuration, is rejected rather than +approximated with broader access when more than one selected policy sets it. +The same rejection +applies to timeout, clipboard, lifecycle, UI, and every other field without an +explicit composition rule. A single selected policy without additions or +dependencies may use catalog-supported fields without cross-policy composition. ## 5. API surface -The standalone libraries separate runtime resolution from catalog inspection. -Resolution accepts one tool or an array and composes all applicable matching -entries and dependencies into one `SandboxPolicy`. Callers choose a policy-only -operation or a diagnostic operation over the same resolution logic. Neither -implicitly returns the whole catalog. These are in-process library calls, not -a hosted service or additions to the MXC SDKs. +The MXC SDK APIs separate runtime resolution from catalog inspection. +Resolution accepts one tool or an array, selects the most specific match for +each input, and composes its effective base, selected intents, and dependencies into one +`SandboxPolicy`. Callers choose a policy-only operation or a diagnostic +operation over the same resolution logic. Neither +implicitly returns the whole catalog. These are SDK library calls, not a +hosted service or a command-line utility. The signatures below use TypeScript to describe the shared contract. Rust and C# expose the same operations and metadata with idiomatic names and types. @@ -487,14 +684,16 @@ callers may ignore it. |---|---|---| | Invalid tool input or resolution context | `malformed_request` | `invalid_context` | | Invalid catalog data, including invalid dependency references or cycles | `policy_validation` | `invalid_catalog` | +| Distinct matches for one tool tied at the highest identity/intent/architecture rank | `policy_validation` | `ambiguous_match` | | Unsupported composition, including mixed policy versions or fields without a composition rule | `policy_validation` | `composition_conflict` | | Unsupported or undetectable host platform or architecture | `unsupported_containment` | `unsupported_host` | -| Catalog content cannot be read or fails its integrity check | `backend_error` | `integrity` | +| Bundled catalog content cannot be read | `backend_error` | `integrity` | | Explicitly requested catalog revision is not installed | `backend_error` | `revision_unavailable` | -Filesystem overlaps handled by [§4.5](#45-dependencies-and-composition) are not -composition failures. Ordinary no-match results remain policy absence, not an -error from this table. +Filesystem and network overlaps handled by +[§4.5](#45-dependencies-and-composition) are not composition failures. Ordinary no-match results remain policy absence, not an +error from this table. A well-typed but unparseable version string or an +unsupported intent is a per-pair diagnostic outcome, not `malformed_request`. ```ts import type { SandboxPolicy } from "@microsoft/mxc-sdk"; @@ -503,6 +702,7 @@ interface ToolCandidate { invocationName: string; packageUrl?: string; detectedVersion?: string; + intent?: string; } type ToolInput = string | ToolCandidate; @@ -516,12 +716,46 @@ interface ResolveContext { allowWeakIdentityFallback?: boolean; } +interface IntentSelection { + requested?: string; + mode: "named" | "all" | "none" | "unsupported"; + selected: string[]; +} + +type VersionStatus = + | "matched_default" + | "matched_version" + | "version_out_of_range" + | "version_unparseable"; + +interface VersionSelection { + status: VersionStatus; + detectedVersion?: string; + selectedVersionRange?: string; +} + +type ToolResolutionStatus = + | VersionStatus + | "intent_unsupported" + | "tool_unmatched"; + +interface ToolResolutionWarning { + code: "version_out_of_range" | "version_unparseable" + | "intent_unsupported" | "tool_unmatched"; + inputIndex: number; + entryId?: string; + detectedVersion?: string; + intent?: string; + message: string; +} + interface SandboxConfigResolution { policy: SandboxPolicy | undefined; diagnostics: { catalogRevision: string; tools: Array<{ inputIndex: number; + status: ToolResolutionStatus; matches: Array<{ entryId: string; entryRevision: number; @@ -529,14 +763,18 @@ interface SandboxConfigResolution { kind: string; strength: "strong" | "weak"; }>; + versionSelection: VersionSelection; + intentSelection?: IntentSelection; }>; }>; resolvedDependencies: Array<{ entryId: string; entryRevision: number; requiredVersionRange?: string; + versionSelection: VersionSelection; + intentSelection: IntentSelection; }>; - warnings: string[]; + warnings: Array; }; } @@ -562,24 +800,47 @@ export declare function resolveSandboxPolicyWithDiagnostics( ``` A string input is shorthand for `{ invocationName: tool }`; it supplies no -package or version evidence and follows the same weak-identity option as an -object input. For example, name-only lookup under the current proposed opt-in +intent, package, or version evidence and follows the same weak-identity option +as an object input. For example, name-only lookup under the opt-in rule is: ```ts -const ctx = { allowWeakIdentityFallback: true }; -const policy = resolveSandboxPolicy("npm", ctx); -const combinedPolicy = resolveSandboxPolicy(["git", "npm"], ctx); -const result = resolveSandboxPolicyWithDiagnostics("npm", ctx); -const combinedResult = - resolveSandboxPolicyWithDiagnostics(["git", "npm"], ctx); +const ctx: ResolveContext = { + 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`, + programData: String.raw`C:\ProgramData`, + temp_dir: String.raw`D:\temp`, + }, +}; +const push = resolveSandboxPolicyWithDiagnostics( + { invocationName: "git", detectedVersion: "2.45", intent: "push" }, ctx); +const bundle = resolveSandboxPolicyWithDiagnostics( + { invocationName: "git", detectedVersion: "2.55", intent: "bundle-fetch" }, ctx); +const noVersionBundle = resolveSandboxPolicyWithDiagnostics( + { invocationName: "git", intent: "bundle-fetch" }, ctx); +const combinedPolicy = resolveSandboxPolicy( + [{ invocationName: "git", detectedVersion: "2.45", intent: "push" }, "node"], ctx); ``` +For the Git entry above, with referenced dependencies available: + +| Request | Selected version data | Result | +|---|---|---| +| `2.45` + `push` | Default + Windows additions + `vers:intdot/>=2.40\|<2.50` | `matched_version`; push policy plus the SSH dependency's default and Windows base additions | +| `2.55` + `bundle-fetch` | Default + Windows additions + `vers:intdot/>=2.50\|<3` | `matched_version`; new bundle-fetch policy, no inherited SSH addition | +| No version + `bundle-fetch` | Default + Windows additions | `intent_unsupported`; `policy` is `undefined` for this single-pair call | + Single-tool lookup is equivalent to a one-element array; its diagnostic `inputIndex` is `0`. A caller retaining separate policies per tool can use single-tool calls. A caller wanting one sandbox for several tools passes an -array. Both forms compose every eligible matching entry, not just the -strongest match, and the selected dependencies. +array. Both forms select one most-specific match per input, then compose the +contributing requirements. A string input uses the unversioned default and all +its effective intents, including applicable platform additions. `resolveSandboxPolicy` returns the composed `SandboxPolicy` directly, not a wrapper or a `ContainerConfig`. It is the exact type provided by the corresponding MXC @@ -590,22 +851,43 @@ and warnings from the same resolution pass. Callers choose one operation; retrieving diagnostics does not require a second lookup or process-global "last result" state. -Following #779, an unmatched input contributes no requirements while matched -inputs still contribute. Each input has a diagnostic record; an unmatched -input has an empty `matches` list and a warning. An empty input array or an -all-unmatched lookup produces no policy, not an empty policy: +**The returned policy may cover only a subset of the requested tools.** +`tool_unmatched`, `version_unparseable`, and `intent_unsupported` pairs contribute +nothing; other pairs still resolve. Callers inspect per-input diagnostics to +determine coverage. No wildcard entry fills a missing match, and there is no +`requireAllMatches` option. + +Each input has a diagnostic record in input order. A `tool_unmatched` input +has an empty `matches` list and a warning. An empty array or a lookup with no +contributing pairs produces no policy, not an empty policy: `resolveSandboxPolicy` returns `undefined`, while the diagnostics operation returns a `SandboxConfigResolution` with `policy: undefined`. An empty array has no per-input records. Unresolved required symbols in selected entries prevent a policy from being returned and produce diagnostics; they are not grounds for silently omitting a selected requirement to produce a partial policy. -Multiple matching entries for one input are listed in `matches`, with a -warning identifying that input and the contributing entry IDs. Shared entries -remain attributed to every matching input even though their policy is -composed once. Dependency diagnostics retain each distinct -entry/revision/required-version-range combination, ordered by those fields; -repeated metadata does not mean repeated policy contribution. +Each input's `matches` contains at most one identity-matched entry, including +when its version or intent prevents contribution. Equally specific matches +remain an ambiguity error. `versionSelection` reports the supplied version, +version status, and selected range only for `matched_version`. + +`intentSelection` reports `mode: "named"` or `"all"` and sorted selected names. +An unsupported name uses `"unsupported"` and an empty list. Version parse +failure skips intent resolution. With no intent and no effective intents, +`"all"` has an empty list and the effective base still contributes. + +A contributing pair's status is its version status. Unsupported intent makes +the pair's status `intent_unsupported` while retaining the version status in +`versionSelection`. Structured warnings preserve input order; for an +out-of-range version and unsupported intent, emit `version_out_of_range` then +`intent_unsupported`. Other warnings remain strings. These statuses never +silently select another variant or entry. + +Dependency metadata includes version and intent selections. A dependency +always reports `matched_default`, and `mode: "none"` with an empty list unless +its reference names intents, which report `"named"`. De-duplicate identical +entry/revision/range/selection records and sort by those fields. Shared +contributions remain attributed to each requesting input. When architecture is omitted, diagnostics include a warning naming the effective native system architecture and stating that the tool's architecture @@ -614,13 +896,14 @@ diagnostics describe selection; they do not attest to the installed tool's architecture. The policy-only operation does not expose warnings or attribution; consumers needing them use `resolveSandboxPolicyWithDiagnostics`. -Filesystem composition diagnostics report read-only requirements superseded -by read-write requirements and catalog denies removed to satisfy required -access. Each warning identifies the resolved paths, access classes, and -contributing entry IDs. A removed deny warning names the full removed scope -and explains that other grants may now apply throughout it, not only at the -overlap. Both APIs return the same composed policy; callers needing to review -these adjustments use `resolveSandboxPolicyWithDiagnostics`. +Composition diagnostics report read-only requirements superseded by +read-write requirements, and catalog filesystem and egress denies removed to +satisfy required access. Each warning identifies the resolved paths or rules, +access classes, and contributing entry IDs. A removed deny warning names the +full removed scope and explains that other grants may now apply throughout +it, not only at the overlap. Both APIs return the same composed policy; +callers needing to review these adjustments use +`resolveSandboxPolicyWithDiagnostics`. ### 5.2 Setup and inspection @@ -629,21 +912,38 @@ type CatalogPlatform = "windows" | "linux" | "macos"; type CatalogArchitecture = "x64" | "arm64"; type CatalogIdentityMetadata = - | { kind: "purl"; value: string; versionRange?: string } + | { kind: "purl"; value: string } | { kind: "invocation-name"; names: string[] }; +interface CatalogIntentMetadata { + name: string; + exampleSubcommands?: string[]; + dependencyEntryIds: string[]; +} + +interface CatalogAdditionsMetadata { + dependencyEntryIds: string[]; + intentAdditions: CatalogIntentMetadata[]; + newIntents: CatalogIntentMetadata[]; +} + interface CatalogEntryMetadata { catalogRevision: string; entryId: string; entryRevision: number; displayName: string; + versionScheme: "npm" | "semver" | "pypi" | "nuget" | "intdot"; identity: CatalogIdentityMetadata[]; - platformVariants: Array<{ - platform: CatalogPlatform; - architecture?: CatalogArchitecture; + default: { dependencyEntryIds: string[]; sandboxPolicyVersion: string; + intents: CatalogIntentMetadata[]; + }; + platformVariants: Array; + versionVariants: Array; provenance: { method: string; sourceRevision: string; @@ -656,7 +956,8 @@ export declare function getCatalogInfo(): { catalogSchemaVersion: string; catalo This supports setup UI, catalog browsing, and update decisions without paying the cost of policy resolution, and keeps "give me everything" out of the -runtime lookup path entirely. Metadata exposes selectors, dependency IDs, and +runtime lookup path entirely. Metadata exposes the default and overlay +selectors, inherited/new intent names, subcommand hints, dependency IDs, and provenance, but not an unresolved or resolved policy body. ### 5.3 Consumer obligations @@ -680,94 +981,76 @@ A consumer that uses this API: records matched identities, catalog/entry revisions, warnings, and approval state in its own audit trail. -The catalog libraries never write a consumer's policy store. A consumer's own +The catalog APIs never write a consumer's policy store. A consumer's own capability observation (see [§8](#8-relationship-to-learning-mode)) can produce candidate evidence for a future contribution to this catalog; it is not a mechanism for mutating the catalog at request time. ## 6. Intended repository and packaging boundary -The catalog is intended to live in a new public repository outside -`microsoft/mxc`. Its schema, entries, resolver libraries, contribution history, -validation, and publication workflow belong there. This specification remains -in MXC while the proposed contract is reviewed. No catalog repository or -package is created by this proposal. - -MXC retains the existing `SandboxPolicy` contract and SDK types. Each catalog -library has a required dependency on its language's MXC SDK and constructs -that SDK's policy type. The dependency runs only from the catalog to MXC: -MXC neither references the catalog nor performs catalog lookup. A consumer -passes its final, authorized policy directly to the existing MXC SDK. -Standalone means separate repository, API, and release ownership, not absence -of SDK dependencies. SDK dependencies may bring native build or package -assets; catalog lookup itself does not invoke MXC sandbox execution. -Catalog and library releases using supported SDK contracts do not require -an MXC SDK release or changes to MXC repository governance. +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 Rust implementation in `mxc_policy_store` resolves policies for all SDKs. +V1 policy data is embedded in the MXC native library at build time. The Rust +SDK uses the shared implementation; Node and .NET are thin wrappers over +`mxc_ffi`. V1 performs no dynamic fetching; that is a possible V2 capability. ### 6.1 Library distribution and consumption -The initial library language coverage matches MXC's current first-party SDK -languages, but the packages are owned and released by the catalog project: +The existing MXC SDKs expose resolution and inspection APIs using their +policy types: -| Language | Distribution | MXC SDK dependency and policy type | +| Language | Existing MXC SDK | Current policy type | |---|---|---| -| TypeScript / JavaScript | npm package | `SandboxPolicy` from `@microsoft/mxc-sdk` | -| Rust | Cargo crate | `mxc_sdk::SandboxPolicy` from `mxc-sdk` | -| C# / .NET | NuGet package | `Microsoft.Mxc.Sdk.SandboxPolicy` from `Microsoft.Mxc.Sdk` | +| TypeScript / JavaScript | `@microsoft/mxc-sdk` | `SandboxPolicy` | +| Rust | `mxc-sdk` | `mxc_sdk::SandboxPolicy` | +| C# / .NET | `Microsoft.Mxc.Sdk` | `Microsoft.Mxc.Sdk.SandboxPolicy` | -Repository and package names remain to be selected. This language match does -not require copying MXC's native-binding architecture or exposing sandbox -execution operations. - -Each library declares compatible MXC SDK versions. The catalog and caller must -resolve compatible SDK dependencies with the same policy type identity, -including crate source/version in Rust. Returned policies must use fields and -contract versions supported by that SDK; unsupported data must not be silently -dropped to fit its types. +Returned policies must use fields supported by the containing SDK and its +1.x policy contract. Unsupported data must not be silently dropped to fit its +types. The final API names will follow any MXC policy-type rename. A consumer: -1. Installs and pins the standalone library package for its language. Each - package includes a reviewed default catalog revision for local use. +1. Installs a supporting MXC SDK release for its language, including its + bundled policy data. 2. Calls `getCatalogInfo()` or `listCatalogEntries()` for inspection. `resolveSandboxPolicy()` returns a policy for one tool or an array; `resolveSandboxPolicyWithDiagnostics()` adds match attribution and warnings. 3. Handles policy absence without widening its restrictive baseline. It reviews the composed policy and uses the diagnostics operation when it needs contributing identities, revisions, and warnings, applying the - consumer obligations in [§5.3](#53-consumer-obligations). -4. Supplies its final policy to its chosen execution integration. The catalog - library does not launch a sandbox. - -The library API reference and repository/package READMEs must state: - -> Returns a candidate MXC `SandboxPolicy` combining the access requirements of -> all matching tools and their dependencies. Overlapping filesystem -> requirements use the least restrictive access needed to satisfy the combined -> requirements, including removal of conflicting catalog-provided denies. -> This is not authorization or a guarantee of workflow success. The caller -> decides whether to accept the requested access and must preserve its own -> user, enterprise, and device restrictions. Use -> `resolveSandboxPolicyWithDiagnostics` to review contributing entries and access -> changes, including the full scope of any removed deny. + consumer obligations in [§5.3](#53-consumer-obligations). A returned policy + may cover only a subset of the requested tool/intent pairs. +4. Supplies its final, authorized policy to MXC execution. The resolution API + does not launch a sandbox. + +The SDK API reference and repository/package READMEs must state: + +> Returns a best-effort MXC policy baseline for representative tool workflows, +> not authorization or a guarantee of success or safety. It may request broader +> access. Callers and users review that access and may further constrain or +> override the recommendation; enterprise and device restrictions remain +> authoritative. Diagnostics explain the contributing requirements and changes. +> A returned policy may cover only a subset of requested tools; inspect +> per-input statuses to determine coverage. Lookup is local and does not download updates, contact a hosted service, or -run the candidate tool. `ResolveContext.catalogRevision` selects an available -local revision, not a network lookup; an explicitly requested revision that -is unavailable is an error, not a substitution with a different revision. -An omitted revision uses the library's installed default. - -Catalog revisions are also published as immutable, language-neutral data -artifacts. A library package version identifies the library release, not the -catalog revision or embedded `SandboxPolicy.version`; it declares the catalog -schema and policy versions it supports and reports its bundled -`catalogRevision`. Publishing newer data can update the packages' bundled -revision without changing resolver behavior. Installing an update does not -rewrite a consumer's previously accepted per-tool policies. +run the candidate tool. `ResolveContext.catalogRevision` selects a revision +included in the installed SDK; an unavailable revision is an error, not a +request to download or substitute data. An omitted revision uses the bundled +default. + +V1 catalog updates ship with an MXC release. Revision metadata can identify the +bundled data independently of the SDK package version without implying a +separate artifact delivery channel. Installing an SDK update does not rewrite +a consumer's previously accepted policies. ### 6.2 Cross-language consistency and support -All three libraries use the same catalog format and shared conformance +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, @@ -780,46 +1063,64 @@ 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. The catalog digest format remains shared as -specified in [§10](#10-immutable-revisions). +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. Implementation sharing between languages is a separate -engineering decision; catalog resolution does not move into MXC's engine. +supported platforms. Binding tests exercise the shared native resolver rather +than independent implementations of the resolution rules. -Supporting three languages includes maintaining parity, dependencies, -documentation, and releases, not only writing the initial implementations. -The libraries and catalog have the same limited public-preview horizon and -are intended to retire together when Learning Mode replaces this workflow. +Policy Store is an ongoing SDK capability. Language parity, +documentation, and maintenance belong to the MXC SDK release process. ## 7. Contribution and review -- Catalog contributions are pull requests against the dedicated catalog - repository. No client or SDK can write a catalog entry at runtime. +- 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. -- CI validates schema conformance, exact `SandboxPolicy` version registration, +- 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 requires regression evidence that every supported - tool version still functions under the narrower requirement. -- Library API and implementation contributions are reviewed in the dedicated - catalog repository. These contribution requirements do not require - applications to seek maintainer approval to use the public catalog or - libraries. +- 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 is the intended long-term solution. The known-tool catalog only -reduces immediate first-run failures while that workflow is completed. It is -not a parallel long-term policy platform. +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 @@ -832,10 +1133,8 @@ 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. No runtime submission hook is proposed here. When -Learning Mode can provide the required observation and policy-authoring -experience directly, this catalog should be retired rather than promoted into -a durable platform. +that consumer's design. The SDK provides no runtime submission hook or +automatic mutation of the release-bundled catalog. ## 9. Trust model @@ -851,7 +1150,8 @@ outcomes: 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 parent deny is removed in full under +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. @@ -859,7 +1159,8 @@ 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, integrity-validated revision ([§10](#10-immutable-revisions)). +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 @@ -876,32 +1177,22 @@ 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. -In addition to schema validation, verify the stored revision content, -including shared symbol definitions, against its packaged expected digest -when loading it. A mismatch is an explicit library failure, not a no-match -result. Validated immutable data may be cached; each lookup need not rehash it. -The packaging format defines the digest input consistently across languages, -without requiring a custom JSON parser or canonicalization implementation. - -This lightweight check detects content that no longer matches the packaged -revision metadata. It does not independently authenticate the publisher or -protect against replacing both content and digest; authenticity remains a -property of the trusted package or artifact distribution channel. +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 `SandboxPolicy` or `ContainerConfig` schema. - No change to executor behavior. -- No change to the MXC SDK APIs and no catalog dependency added to MXC. - The catalog libraries depend on MXC SDKs, not the reverse. Catalog lookup - requires an explicit call; existing MXC callers see no behavior change. -- Catalog schema and API compatibility are limited to the stopgap's support - horizon. Retirement in favor of Learning Mode is an expected outcome, not a - normal promotion milestone. +- 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 -**Resolver libraries (TypeScript/JavaScript, Rust, and C#/.NET)** +**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 @@ -911,8 +1202,29 @@ property of the trusted package or artifact distribution channel. any supplied reason uses its listed value, and callers can handle the code alone - one-tool and one-element-array overloads produce equivalent policies and diagnostics; the simple API returns the same policy as the diagnostic API -- multiple input tools compose all matching entries and dependencies into one - policy; repeated inputs or shared dependencies do not duplicate contributions +- 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 +- 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 +- 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 +- 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 @@ -927,34 +1239,33 @@ property of the trusted package or artifact distribution channel. 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 -- one input matching several entries composes all eligible matches, including - equal-strength matches; a stronger match does not suppress a weaker eligible - match, and diagnostics preserve all matching entries and identity evidence +- 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; additive matching does not bypass it -- invocation-name case variants match identically on all platforms without - changing command spelling or package-identity matching + 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 - 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 -- version-range mismatch produces a warning, not a refusal -- exact-architecture variant precedes the platform-only variant; duplicate - selectors are rejected; no matching variant produces `undefined` +- 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 variant falls back to the platform's neutral variant; - a different architecture's variant is never used as a fallback +- 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 diagnostics specified in [§5.1](#51-runtime-lookup); host-architecture detection failure produces a library error, not a guessed match - dependency chain resolution, including cycles (terminate, no duplication) -- dependency `versionRange` is returned as unevaluated metadata and never used - for v1 resolver matching - 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 @@ -968,32 +1279,54 @@ property of the trusted package or artifact distribution channel. - 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 -- mixed policy versions, network fields, and other unsupported composed fields - remain rejected, including incompatible entries selected together only at - lookup time +- 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 +- mixed policy versions 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 entry and platform variant validates against the catalog schema and - the `SandboxPolicy` schema for its declared version -- `dependencies[].entryId` references resolve within the same catalog revision +- every default and materialized platform/architecture/version/intent + combination validates against the catalog and MXC policy schemas +- 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 inherit the policy version 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 proposed change +- catalog/entry revision monotonicity across a change **Integration** -- each package installs with its declared MXC SDK dependency and performs - lookup without invoking sandbox execution; lookup requires no network access +- 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 and the diagnostic result's policy to existing MXC SDK APIs without casts, adapters, or serialization; the same SDK policy type is used throughout - the bundled catalog revision matches `getCatalogInfo()`; selecting an unavailable revision fails explicitly, without falling back to another revision -- changing stored catalog content without updating its expected digest fails - on load even when the changed content still passes schema validation -- a package update leaves previously accepted consumer policies unchanged +- 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 @@ -1001,22 +1334,16 @@ property of the trusted package or artifact distribution channel. 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 deny still prevents access even when an overlapping - catalog-provided deny was removed during floor composition +- a caller-owned filesystem or egress deny still prevents access even when an + overlapping catalog-provided deny was removed during floor composition ## 13. Open questions -Recommended answers are proposals for review, not decisions. - | Question | Recommended answer | |---|---| -| What is the dedicated repository name and owning team? | Use a public repository outside `microsoft/mxc`; publish a separately versioned artifact so catalog updates are not coupled to SDK releases. | -| Is invocation-name-only identity accepted automatically, or does it require explicit consumer opt-in? | Treat it as a fallback requiring explicit opt-in (`allowWeakIdentityFallback`), not the default. | -| What happens on a detected tool-version mismatch: `undefined`, or a warning-bearing result the consumer may still use? | Return the resolved result with a warning; refusing outright removes information the consumer needs to decide for itself. | -| Are private or enterprise catalog overlays in scope, and if so with what precedence? | Defer until the shared catalog contract and its API are stable; define precedence explicitly before any library implementation adds overlay support. | -| Should the first contract version's composition vocabulary expand beyond [§4.5](#45-dependencies-and-composition) before implementation? | No. Start with least-restrictive filesystem floor composition and expand only with an explicit, reviewed rule per field. | -| Who owns catalog schema, data, and library API review? | Assign catalog, library, and security reviewers in the dedicated repository; MXC SDKs remain unaware of the catalog. | -| Should the libraries share a resolver implementation or implement the contract independently? | Choose based on dependency footprint and maintenance cost, with shared conformance fixtures required either way. | +| What are the final API names? | Follow MXC's policy-type rename that drops `Sandbox`; align the resolver names with it. | +| Which concrete MXC schema or validator checks entries and maps their version metadata to the SDK-selected 1.x contract? | Use authoritative MXC validation, not a catalog-specific policy vocabulary; identify the exact entry point or artifact. | +| Which Package URL normalization and equality rules apply? | Specify the standard and treatment of encoding, case, qualifiers, versions, subpaths, and malformed input. | ## 14. Related work @@ -1030,3 +1357,5 @@ Recommended answers are proposals for review, not decisions. the `SandboxPolicy` contract every catalog entry embeds. - [`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. From f7a450c04a28a5346d67e8aa6ae538a91b74665a Mon Sep 17 00:00:00 2001 From: "Chaz Gordish (Agent)" Date: Tue, 6 Oct 2026 10:37:00 -0700 Subject: [PATCH 11/12] docs: align policy store with MXC v1 requirements API Keep lookup command-free with ContainerRequirements using existing v1 SDK field types. Clarify validation, structured diagnostics, PURL matching, contribution attribution, and filesystem identity while preserving the catalog composition and partial-result contract. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/mxc-policy-store.md | 534 ++++++++++++++++++++++++++++----------- 1 file changed, 386 insertions(+), 148 deletions(-) diff --git a/docs/mxc-policy-store.md b/docs/mxc-policy-store.md index 3ae1fb15a..7d678d625 100644 --- a/docs/mxc-policy-store.md +++ b/docs/mxc-policy-store.md @@ -14,17 +14,16 @@ further constrain or override the recommendation. ## 1. Problem Statement -Developers frequently disable process isolation after enabling it breaks tools -needed for their workflow. This makes the first-run experience for process -containment poor and reduces adoption before developers can identify the -missing policy. - -The solution is a reviewed set of policy floors bundled with MXC and -exposed through its SDKs. Consumers can apply a best-effort baseline instead of -starting from no tool knowledge, and contributors can improve the data as -failures are found. A floor describes what is believed to be needed for -representative cases. It does not prove that every process, dependency, -credential, service, or network interaction in an end-to-end workflow is covered. +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 @@ -33,14 +32,14 @@ inspection behavior. It reuses #779's model where possible and calls out differences directly. This document does not restate general MXC sandboxing concepts already covered -by [`docs/sandbox-policy/0.8.0/policy.md`](sandbox-policy/0.8.0/policy.md) or +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 what the sandbox backend enforces, or `SandboxPolicy` / - `ContainerConfig` schema semantics. A catalog entry embeds an existing - `SandboxPolicy`; it does not define a parallel vocabulary. +- 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 @@ -52,23 +51,38 @@ by [`docs/sandbox-policy/0.8.0/policy.md`](sandbox-policy/0.8.0/policy.md) or ### 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 MXC's policy schema. +- **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 the SDK's policy type. + 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). Defaults and omission behavior are: +- `ResolveContext` is optional and carries lookup context only. No command or + execution settings are required, inferred, or returned by the resolver. - Existing callers do not perform catalog lookup automatically. A consumer must explicitly enable or invoke it. - Omitted `ResolveContext.platform` uses the current host platform. @@ -90,8 +104,8 @@ Defaults and omission behavior are: - Omitted `ToolCandidate.intent` selects the base plus all intents of the effective version policy. An unsupported intent contributes no policy and produces an `intent_unsupported` warning. -- If no policy can be resolved, `resolveSandboxPolicy` returns `undefined`. - `resolveSandboxPolicyWithDiagnostics` instead returns a result whose `policy` is +- If no policy can be resolved, `resolveToolRequirements` yields `undefined`. + `resolveToolRequirementsWithDiagnostics` yields a result whose `requirements` is `undefined`, preserving the diagnostics. The consumer's restrictive baseline remains unchanged. @@ -99,7 +113,7 @@ Defaults and omission behavior are: MXC owns the reviewed, versioned, read-only catalog, its resolution and inspection APIs, and their SDK publication lifecycle in `microsoft/mxc`. -The policy contract and returned SDK type remain MXC-owned as well. +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. @@ -115,11 +129,11 @@ Everything else is a consumer decision: 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 into `createConfigFromPolicy()` / - sandbox creation. +- 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. `resolveSandboxPolicy` returns a candidate composed requirement or +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 @@ -131,11 +145,11 @@ required to grant. | #779 (config floors) | This document (policy store) | |---|---| -| One `schemaVersion` for the whole table | Four separate version dimensions: `catalogSchemaVersion`, `catalogRevision`, per-entry `entryRevision`, and `default.sandboxPolicy.version` ([§4.1](#41-versions)) | +| 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 `resolveSandboxPolicy`, accepting one tool or an array and returning one policy; `resolveSandboxPolicyWithDiagnostics` adds attribution, with catalog inspection kept separate ([§5](#5-api-surface)) | +| `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, @@ -152,19 +166,26 @@ are stated here rather than implied by that reference. | `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. | -| `default.sandboxPolicy.version` | Policy-schema version inherited by all additions for the entry. | +| `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 `SandboxPolicy` contract does not change existing catalog -data and therefore does not require a new catalog revision. Migrating an -entry to that contract changes the entry's content, so publication -of that migration must increment both `entryRevision` and `catalogRevision`. -A catalog revision may still change without incrementing unaffected entries. +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 policy schema and remain compatible within MXC 1.x; -breaking policy-schema changes require 2.x. The SDK selects the exact -configuration contract as described in [versioning.md](versioning.md). +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 @@ -179,8 +200,7 @@ configuration contract as described in [versioning.md](versioning.md). { "kind": "invocation-name", "names": ["git", "git.exe"] } ], "default": { - "sandboxPolicy": { - "version": "0.9.0-alpha", + "requirements": { "filesystem": { "readonlyPaths": ["${git_prefix}"], "readwritePaths": ["${project_root}"] @@ -257,9 +277,10 @@ configuration contract as described in [versioning.md](versioning.md). 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 a minimal base `sandboxPolicy` and intent additions. -Unversioned means no tool-version selector; the policy-schema version remains -separate. +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 @@ -315,13 +336,36 @@ Invariants: - 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 policies use MXC's policy schema, not a separate catalog policy - vocabulary. [§13](#13-open-questions) tracks the concrete validator selection. +- 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 `SandboxPolicy`. Entries continue to reference +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: @@ -383,6 +427,22 @@ not prove that the installed tool belongs to that package; the caller is responsible for verifying that association. The library does not inspect the tool to verify it. +Parse candidate and catalog package URLs using the +[PURL component rules](https://github.com/package-url/purl-spec/blob/7cd2d3442fb9c88155db17ada7c911b40ec22d41/docs/specification/standard/Clause-5-Package-URL-Specification.md) +and applicable type definitions, including percent-decoding before comparison. +Compare only type, namespace, and name. Catalog comparison is +locale-independent and case-insensitive for type and namespace; the name +follows its package type's normalization and case rules. Namespace folding is +a catalog lookup rule, not a claim that every ecosystem treats namespaces +case-insensitively. + +Ignore candidate version, qualifiers, and subpath for matching and record +`purl_components_ignored` when any is present. Only `detectedVersion` supplies +tool-version evidence. Validate the complete PURL before ignoring components. +An invalid candidate PURL makes that pair `tool_unmatched` with `purl_invalid`; +do not repair it, retry invocation-name matching, or fail other pairs. +Invalid catalog PURLs are catalog validation errors. + Invocation-name matching is locale-independent and case-insensitive on Windows and macOS, and exact on Linux. This does not change the caller's command or package-identity matching. @@ -476,7 +536,7 @@ same `policyAdditions`, `dependencies`, `intentAdditions`, and `newIntents` fields as a version overlay. It never removes, narrows, or replaces default requirements. Select at most one platform/architecture overlay, then combine its additions with the default and the selected version overlay. -Architecture is a catalog selector, not a field added to `SandboxPolicy`. Omitting +Architecture is a catalog selector, not a field added to `ContainerRequest`. Omitting `when.architecture` makes a catalog variant architecture-neutral; omitting the caller's `ResolveContext.architecture` instead requests the host default. @@ -539,10 +599,15 @@ policy. Compose each selected base with its selected intent additions, then combine the results for different requested tools and their transitive dependencies. -De-duplicate contributions by catalog revision, entry ID, selected version -range (or default), platform variant, and intent name, not by entry ID alone. -Repeated identical components contribute once; different requested versions -or intents retain their own additions. +De-duplicate each source contribution layer independently within an entry and +catalog revision. The default base contributes once, and each selected platform +base layer contributes once, regardless of requested version or intent. +Default intent additions are keyed by intent; platform intent additions by +platform selector and intent; version base additions by range; and version +intent additions by range and intent. Include new-intent definitions under +their owning overlay. Never attach the requesting pair's version or intent to +a shared base-layer key. Repeated identical layers contribute once; distinct +version and intent additions selected by different pairs are retained. Preserve per-input attribution. A shared `ResolveContext` applies to the whole lookup; each tool candidate carries its own intent. @@ -555,12 +620,13 @@ Filesystem composition uses the exact `filesystem.deniedPaths`, `filesystem.readonlyPaths`, and `filesystem.readwritePaths` fields: -1. Every base policy in the selected entries and their dependency closure must - declare the same `sandboxPolicy.version`; intent additions inherit it. +1. All selected entries and additions use the containing catalog revision's + validated SDK target; no stored request contains a `version`. 2. At lookup time, resolve all required symbols and normalize paths using the selected platform's path rules before comparing equal or ancestor/descendant paths. Combine the selected entries and dependencies, - de-duplicating paths within each access class. + de-duplicating equivalent pathnames within each access class, not distinct + required alias locations. 3. Preserve every required read-write subtree. Remove read-only entries equal to or contained within a read-write subtree, since read-write already satisfies their read requirement. Retain a read-only ancestor of a @@ -578,10 +644,48 @@ Filesystem comparison is separate from invocation-name matching. Equality, de-duplication, and ancestor checks honor the applicable filesystem and directory case-sensitivity, not a blanket OS assumption. When that information cannot be determined, compare case-sensitively, preserve differently cased -paths as distinct, and report the assumption in diagnostics. Returned paths +paths for comparison, and report the assumption in diagnostics. Returned paths retain their casing; comparison must not lowercase the policy paths. Another target environment must not inherit this host's filesystem case rules. +Resolve actual filesystem object identity using MXC's +[object comparison primitives](https://github.com/microsoft/mxc/blob/894f4c159705f5f470727e4fa1e363a2abec88f1/src/mxc-sdk/src/core/mxc_common/filesystem_object.rs#L100-L265). +Symlink, junction, hard-link, bind-mount, and 8.3 aliases must participate in +the same catalog access composition, not silently cause a required read-write +path to become read-only or denied when MXC later normalizes the request. +Keep this floor composition separate from the runner's restrictive enforcement. + +Object identity reconciles access, not pathname reachability. Retain every +required alias location even when multiple paths name the same object. If a +read-only alias names an object required read-write through another path, +retain that alias with the composed read-write access instead of deleting it. +Do not collapse same-class aliases merely because their object identities +match: path-based backends still need each required mount or pathname. + +If necessary identity cannot be established for the target, that pair +contributes nothing and reports `filesystem_identity_unresolved`; other +independently resolved pairs can still contribute. This includes unresolved +aliases introduced by dependencies or cross-pair composition. Do not treat +unknown identity as proof that paths differ, bypass this check with lexical +case rules, or weaken MXC enforcement. + +V1 inspects local host-side source paths, not a remote or guest filesystem. +Windows identity uses volume serial number and file ID through +`CreateFileW`/`FileIdInfo`; Unix uses `stat` device/inode (following links). +Compare opened/resolved objects, preserve every required alias pathname, and +compare existing ancestor identities for subtree overlap. A cleanly missing +suffix is compared relative to its deepest resolvable existing ancestor; it +is not treated as an existing alias. An unreadable component, broken link +whose target cannot be established, or different target filesystem is unknown +and drops every input whose access relationship depends on that fact. Never +drop only the restrictive side of an unresolved relation to retain its grant. +Case-sensitive comparison is only a lexical aid when case sensitivity is +unknown, not permission to skip object checks. Apply the same analysis across +pair/dependency boundaries. Discard all layers solely owned by excluded pairs, +then compose the remaining complete pairs; retain shared layers only for +remaining owners. No lookup result bypasses the runner's authoritative +enforcement-time checks or claims protection from later filesystem changes. + | Resolved requirements | Composed filesystem policy | |---|---| | Read-only `/work` and read-write `/work` | Read-write `/work`; omit read-only `/work` | @@ -590,7 +694,7 @@ target environment must not inherit this host's filesystem case rules. | Denied `/data` and read-write `/data/cache` | Remove denied `/data`; retain read-write `/data/cache` and report that the entire `/data` deny was removed | | Denied `/secrets` and read-write `/work` | Retain both non-overlapping entries | -These are composition rules for the returned MXC `SandboxPolicy`, not changes +These are composition rules for the returned `ContainerRequirements`, not changes to MXC's enforcement precedence. Simply concatenating a conflicting deny or read-only entry with a grant is insufficient: the restrictive entry could still prevent the access the composed floor is intended to request. @@ -655,7 +759,7 @@ dependencies may use catalog-supported fields without cross-policy composition. The MXC SDK APIs separate runtime resolution from catalog inspection. Resolution accepts one tool or an array, selects the most specific match for each input, and composes its effective base, selected intents, and dependencies into one -`SandboxPolicy`. Callers choose a policy-only operation or a diagnostic +`ContainerRequirements`. Callers choose a requirements-only operation or a diagnostic operation over the same resolution logic. Neither implicitly returns the whole catalog. These are SDK library calls, not a hosted service or a command-line utility. @@ -669,6 +773,27 @@ absent policy is `undefined` in TypeScript/JavaScript, `None` in Rust, and ### 5.1 Runtime lookup +`ContainerRequirements` preserves the earlier four-field scope: filesystem, +network, UI, and timeout, with the composition rules in §4.5. It reuses the +SDK's nested types and optionality; `Pick` is not runtime validation or an +expansion of the catalog's supported fields (§4.2). Rust/.NET use an equivalent +four-field aggregate, not duplicate nested models. + +Context is optional and lookup-only. Command, containment, name, working +directory, environment, cleanup, proxy, and operation options remain caller-owned, +not resolver inputs or outputs. Intent selects requirements, not a command. +Resolve again or supply overrides if the eventual execution environment differs +from discovery. Requirements do not certify coverage of an arbitrary command. + +Node resolution uses Promise-returning plain verbs, matching the v1 +[run/spawn convention](https://github.com/microsoft/mxc/blob/894f4c159705f5f470727e4fa1e363a2abec88f1/sdk/node/src/v1/container.ts#L428-L477); +filesystem work must not block the event loop. Rust exposes +`v1::resolve_tool_requirements` / `resolve_tool_requirements_with_diagnostics` +as `Result, Error>` / `Result`. +.NET exposes `MxcContainer.ResolveToolRequirements` and +`ResolveToolRequirementsWithDiagnostics` (plus `Async` Task forms) in `V1`. +Metadata inspection stays synchronous; no operation creates a container. + Failures reuse existing MXC error codes, which are sufficient for normal programmatic handling. An optional `details.reason` may provide a stable, catalog-specific distinction for logging, investigation, or finer handling @@ -685,18 +810,22 @@ callers may ignore it. | Invalid tool input or resolution context | `malformed_request` | `invalid_context` | | Invalid catalog data, including invalid dependency references or cycles | `policy_validation` | `invalid_catalog` | | Distinct matches for one tool tied at the highest identity/intent/architecture rank | `policy_validation` | `ambiguous_match` | -| Unsupported composition, including mixed policy versions or fields without a composition rule | `policy_validation` | `composition_conflict` | +| Unsupported composition, including incompatible SDK target metadata or fields without a composition rule | `policy_validation` | `composition_conflict` | | Unsupported or undetectable host platform or architecture | `unsupported_containment` | `unsupported_host` | | Bundled catalog content cannot be read | `backend_error` | `integrity` | | Explicitly requested catalog revision is not installed | `backend_error` | `revision_unavailable` | Filesystem and network overlaps handled by [§4.5](#45-dependencies-and-composition) are not composition failures. Ordinary no-match results remain policy absence, not an -error from this table. A well-typed but unparseable version string or an -unsupported intent is a per-pair diagnostic outcome, not `malformed_request`. +error from this table. Invalid candidate PURLs, well-typed but unparseable +version strings, unsupported intents, and unresolved filesystem identity are +per-pair diagnostic outcomes, not whole-request errors from this table. ```ts -import type { SandboxPolicy } from "@microsoft/mxc-sdk"; +import type { ContainerRequest, NetworkRuleConfig } from "@microsoft/mxc-sdk/v1"; + +export type ContainerRequirements = Pick; interface ToolCandidate { invocationName: string; @@ -737,20 +866,65 @@ interface VersionSelection { type ToolResolutionStatus = | VersionStatus | "intent_unsupported" - | "tool_unmatched"; - -interface ToolResolutionWarning { - code: "version_out_of_range" | "version_unparseable" - | "intent_unsupported" | "tool_unmatched"; - inputIndex: number; - entryId?: string; - detectedVersion?: string; - intent?: string; + | "tool_unmatched" + | "filesystem_identity_unresolved"; + +type InputWarning = { inputIndex: number; message: string }; +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 } +); + +interface WarningScope { + inputIndexes: number[]; + entryIds: string[]; message: string; } -interface SandboxConfigResolution { - policy: SandboxPolicy | undefined; +interface PathRequirement { + path: string; + access: "denied" | "readonly" | "readwrite"; + entryIds: string[]; +} + +type EgressRule = + NetworkRuleConfig; + +interface NetworkRequirement { + rule: EgressRule; + entryIds: string[]; +} + +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[] } +); + +type PolicyResolutionWarning = ToolResolutionWarning | ResolutionDetailWarning; + +interface ToolRequirementsResolution { + requirements: ContainerRequirements | undefined; diagnostics: { catalogRevision: string; tools: Array<{ @@ -770,33 +944,34 @@ interface SandboxConfigResolution { resolvedDependencies: Array<{ entryId: string; entryRevision: number; + inputIndexes: number[]; requiredVersionRange?: string; versionSelection: VersionSelection; intentSelection: IntentSelection; }>; - warnings: Array; + warnings: PolicyResolutionWarning[]; }; } -export declare function resolveSandboxPolicy( +export declare function resolveToolRequirements( tool: ToolInput, ctx?: ResolveContext -): SandboxPolicy | undefined; +): Promise; -export declare function resolveSandboxPolicy( +export declare function resolveToolRequirements( tools: readonly ToolInput[], ctx?: ResolveContext -): SandboxPolicy | undefined; +): Promise; -export declare function resolveSandboxPolicyWithDiagnostics( +export declare function resolveToolRequirementsWithDiagnostics( tool: ToolInput, ctx?: ResolveContext -): SandboxConfigResolution; +): Promise; -export declare function resolveSandboxPolicyWithDiagnostics( +export declare function resolveToolRequirementsWithDiagnostics( tools: readonly ToolInput[], ctx?: ResolveContext -): SandboxConfigResolution; +): Promise; ``` A string input is shorthand for `{ invocationName: tool }`; it supplies no @@ -817,14 +992,31 @@ const ctx: ResolveContext = { temp_dir: String.raw`D:\temp`, }, }; -const push = resolveSandboxPolicyWithDiagnostics( +const push = await resolveToolRequirementsWithDiagnostics( { invocationName: "git", detectedVersion: "2.45", intent: "push" }, ctx); -const bundle = resolveSandboxPolicyWithDiagnostics( +const bundle = await resolveToolRequirementsWithDiagnostics( { invocationName: "git", detectedVersion: "2.55", intent: "bundle-fetch" }, ctx); -const noVersionBundle = resolveSandboxPolicyWithDiagnostics( +const noVersionBundle = await resolveToolRequirementsWithDiagnostics( { invocationName: "git", intent: "bundle-fetch" }, ctx); -const combinedPolicy = resolveSandboxPolicy( +const combinedRequirements = await resolveToolRequirements( [{ invocationName: "git", detectedVersion: "2.45", intent: "push" }, "node"], ctx); +for (const warning of push.diagnostics.warnings) { + if (warning.code === "filesystem_deny_removed") { + console.log(warning.removed.path, warning.requiredBy, warning.entryIds); + } +} +``` + +Later, reviewed and constrained requirements combine with a command without +converting their nested types: + +```ts +function createRequest( + approvedRequirements: ContainerRequirements, + command: string, +): ContainerRequest { + return { ...approvedRequirements, command }; +} ``` For the Git entry above, with referenced dependencies available: @@ -833,7 +1025,7 @@ For the Git entry above, with referenced dependencies available: |---|---|---| | `2.45` + `push` | Default + Windows additions + `vers:intdot/>=2.40\|<2.50` | `matched_version`; push policy plus the SSH dependency's default and Windows base additions | | `2.55` + `bundle-fetch` | Default + Windows additions + `vers:intdot/>=2.50\|<3` | `matched_version`; new bundle-fetch policy, no inherited SSH addition | -| No version + `bundle-fetch` | Default + Windows additions | `intent_unsupported`; `policy` is `undefined` for this single-pair call | +| No version + `bundle-fetch` | Default + Windows additions | `intent_unsupported`; `requirements` is `undefined` for this single-pair call | Single-tool lookup is equivalent to a one-element array; its diagnostic `inputIndex` is `0`. A caller retaining separate policies per tool can use @@ -842,26 +1034,25 @@ array. Both forms select one most-specific match per input, then compose the contributing requirements. A string input uses the unversioned default and all its effective intents, including applicable platform additions. -`resolveSandboxPolicy` returns the composed `SandboxPolicy` directly, not a wrapper -or a `ContainerConfig`. It is the exact type provided by the corresponding MXC -SDK, not a catalog-owned lookalike. Callers can pass an accepted policy directly -to that SDK without conversion or serialization. The `policy` field returned -by `resolveSandboxPolicyWithDiagnostics` uses that same SDK type, with attribution -and warnings from the same resolution pass. Callers choose one operation; -retrieving diagnostics does not require a second lookup or process-global -"last result" state. - -**The returned policy may cover only a subset of the requested tools.** -`tool_unmatched`, `version_unparseable`, and `intent_unsupported` pairs contribute -nothing; other pairs still resolve. Callers inspect per-input diagnostics to -determine coverage. No wildcard entry fills a missing match, and there is no -`requireAllMatches` option. +Both operations yield the same requirements; the diagnostics form adds +attribution and warnings from that resolution pass. No second lookup or +process-global "last result" state is needed. + +**The returned requirements may cover only a subset of the requested tools.** +Partial results are intentional in both APIs. `tool_unmatched`, +`version_unparseable`, `intent_unsupported`, and +`filesystem_identity_unresolved` pairs contribute nothing; other pairs still +resolve. The requirements-only call makes no coverage promise, even when its result +is non-`undefined`. Callers needing to know which pairs contributed use +`resolveToolRequirementsWithDiagnostics` and inspect per-input statuses. +No wildcard entry fills a missing match, and there is no `requireAllMatches` +option. Each input has a diagnostic record in input order. A `tool_unmatched` input has an empty `matches` list and a warning. An empty array or a lookup with no -contributing pairs produces no policy, not an empty policy: -`resolveSandboxPolicy` returns `undefined`, while the diagnostics operation returns -a `SandboxConfigResolution` with `policy: undefined`. An empty array has no +contributing pairs produces no requirements, not an empty requirements object: +`resolveToolRequirements` yields `undefined`, while the diagnostics operation yields +a `ToolRequirementsResolution` with `requirements: undefined`. An empty array has no per-input records. Unresolved required symbols in selected entries prevent a policy from being returned and produce diagnostics; they are not grounds for silently omitting a selected requirement to produce a partial policy. @@ -876,25 +1067,39 @@ An unsupported name uses `"unsupported"` and an empty list. Version parse failure skips intent resolution. With no intent and no effective intents, `"all"` has an empty list and the effective base still contributes. -A contributing pair's status is its version status. Unsupported intent makes +A contributing pair's status is its version status. Unresolved target object +identity makes its status `filesystem_identity_unresolved` while preserving +any completed version and intent selection. Unsupported intent makes the pair's status `intent_unsupported` while retaining the version status in `versionSelection`. Structured warnings preserve input order; for an out-of-range version and unsupported intent, emit `version_out_of_range` then -`intent_unsupported`. Other warnings remain strings. These statuses never -silently select another variant or entry. +`intent_unsupported`. All warnings are structured records; `code` selects the +category's fields and `message` is human-readable, not a parsing contract. +These statuses never silently select another variant or entry. + +Per-input warning fields retain the prototype's `inputIndex`, `entryId`, +`detectedVersion`, `intent`, and `message` vocabulary where applicable. +Warnings about shared contributions use sorted, distinct `inputIndexes` and +`entryIds`; each path/rule contribution also identifies its source entries. +`removed` always contains the complete original deny path or egress rule, +including exclusions and protocol/port selectors, not merely the intersection. +Removing it can affect its entire scope wherever other grants apply. Dependency metadata includes version and intent selections. A dependency always reports `matched_default`, and `mode: "none"` with an empty list unless its reference names intents, which report `"named"`. De-duplicate identical entry/revision/range/selection records and sort by those fields. Shared -contributions remain attributed to each requesting input. +contributions retain sorted, distinct `inputIndexes` for every contributing +requester, including transitive dependencies. Union these indexes when +de-duplicating identical records; requester indexes are not part of the +dependency-record identity. When architecture is omitted, diagnostics include a warning naming the effective native system architecture and stating that the tool's architecture was not verified. Architecture-neutral fallback is also identified. These diagnostics describe selection; they do not attest to the installed tool's -architecture. The policy-only operation does not expose warnings or -attribution; consumers needing them use `resolveSandboxPolicyWithDiagnostics`. +architecture. The requirements-only operation does not expose warnings or +attribution; consumers needing them use `resolveToolRequirementsWithDiagnostics`. Composition diagnostics report read-only requirements superseded by read-write requirements, and catalog filesystem and egress denies removed to @@ -903,7 +1108,7 @@ access classes, and contributing entry IDs. A removed deny warning names the full removed scope and explains that other grants may now apply throughout it, not only at the overlap. Both APIs return the same composed policy; callers needing to review these adjustments use -`resolveSandboxPolicyWithDiagnostics`. +`resolveToolRequirementsWithDiagnostics`. ### 5.2 Setup and inspection @@ -936,7 +1141,6 @@ interface CatalogEntryMetadata { identity: CatalogIdentityMetadata[]; default: { dependencyEntryIds: string[]; - sandboxPolicyVersion: string; intents: CatalogIntentMetadata[]; }; platformVariants: Array`) | +| Rust | `mxc-sdk` | `mxc_sdk::v1::ContainerRequirements`, using v1 section types | +| C# / .NET | `Microsoft.Mxc.Sdk.V1` | `ContainerRequirements`, using v1 section types | -Returned policies must use fields supported by the containing SDK and its -1.x policy contract. Unsupported data must not be silently dropped to fit its -types. The final API names will follow any MXC policy-type rename. +Returned requirements use the containing SDK's supported fields. Unsupported +data must not be silently dropped to fit its types. A consumer: -1. Installs a supporting MXC SDK release for its language, including its - bundled policy data. -2. Calls `getCatalogInfo()` or `listCatalogEntries()` for inspection. - `resolveSandboxPolicy()` returns a policy for one tool or an array; - `resolveSandboxPolicyWithDiagnostics()` adds match attribution and warnings. -3. Handles policy absence without widening its restrictive baseline. It - reviews the composed policy and uses the diagnostics operation when it - needs contributing identities, revisions, and warnings, applying the - consumer obligations in [§5.3](#53-consumer-obligations). A returned policy - may cover only a subset of the requested tool/intent pairs. -4. Supplies its final, authorized policy to MXC execution. The resolution API - does not launch a sandbox. +1. Installs an MXC SDK release with its bundled catalog. +2. Inspects the catalog or resolves requirements using §5's APIs. +3. Reviews access and coverage, preserving its restrictive baseline on absence + and applying [§5.3](#53-consumer-obligations). +4. Supplies command/execution settings later, applies its restrictions, and + passes the resulting `ContainerRequest` to MXC. The SDK API reference and repository/package READMEs must state: @@ -1183,7 +1383,7 @@ separate catalog digest or runtime checksum. Schema validation still applies. ## 11. Backward compatibility -- No change to `SandboxPolicy` or `ContainerConfig` schema. +- 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. @@ -1200,11 +1400,18 @@ separate catalog digest or runtime checksum. Schema validation still applies. consistent and uses the corresponding MXC error codes - failure cases use the primary-code mappings in [§5.1](#51-runtime-lookup); any supplied reason uses its listed value, and callers can handle the code alone -- one-tool and one-element-array overloads produce equivalent policies and - diagnostics; the simple API returns the same policy as the diagnostic API +- one-tool and one-element-array overloads produce equivalent requirements and + diagnostics; the simple API yields the same requirements as the diagnostic API +- 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` @@ -1218,17 +1425,21 @@ separate catalog digest or runtime checksum. Schema validation still applies. 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 context uses host platform and native system architecture, the +- 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, @@ -1250,6 +1461,11 @@ separate catalog digest or runtime checksum. Schema validation still applies. 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 and namespace compare case-insensitively, names follow their + type's rules, 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 @@ -1273,6 +1489,19 @@ separate catalog digest or runtime checksum. Schema validation still applies. - 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 @@ -1288,7 +1517,7 @@ separate catalog digest or runtime checksum. Schema validation still applies. 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 -- mixed policy versions and network fields other than egress allow/deny rules, +- 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 @@ -1296,12 +1525,19 @@ separate catalog digest or runtime checksum. Schema validation still applies. **Data (CI)** - every default and materialized platform/architecture/version/intent - combination validates against the catalog and MXC policy schemas + combination passes the closed catalog checks, typed v1 builder, semantic + validation, and exact schema validation; no backend/probe is needed +- 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 inherit the policy version and cannot +- 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 @@ -1321,8 +1557,9 @@ separate catalog digest or runtime checksum. Schema validation still applies. - 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 and the diagnostic result's policy to existing MXC SDK APIs without - casts, adapters, or serialization; the same SDK policy type is used throughout + 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 @@ -1336,14 +1573,14 @@ separate catalog digest or runtime checksum. Schema validation still applies. 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 -| Question | Recommended answer | +| Maintainer sign-off | Recommended answer | |---|---| -| What are the final API names? | Follow MXC's policy-type rename that drops `Sandbox`; align the resolver names with it. | -| Which concrete MXC schema or validator checks entries and maps their version metadata to the SDK-selected 1.x contract? | Use authoritative MXC validation, not a catalog-specific policy vocabulary; identify the exact entry point or artifact. | -| Which Package URL normalization and equality rules apply? | Specify the standard and treatment of encoding, case, qualifiers, versions, subpaths, and malformed input. | +| Approve the command-free requirements API surface? | `resolveToolRequirements` / `resolveToolRequirementsWithDiagnostics` return `ContainerRequirements` using existing v1 section types, with optional lookup context, Promise-based Node resolution, corresponding Rust/.NET bindings, and the documented partial-result contract. | ## 14. Related work @@ -1353,8 +1590,9 @@ separate catalog digest or runtime checksum. Schema validation still applies. - [`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. -- [`docs/sandbox-policy/0.8.0/policy.md`](sandbox-policy/0.8.0/policy.md) - - the `SandboxPolicy` contract every catalog entry embeds. +- [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) - From 5c2ba8e60ade041ca7893670985d61009a22b9ff Mon Sep 17 00:00:00 2001 From: "Chaz Gordish (Agent)" Date: Tue, 6 Oct 2026 17:30:21 -0700 Subject: [PATCH 12/12] docs: split policy store API contract from catalog design Add focused API types, behavior tables, and Zava Agent and shared-build examples. Document per-tool catalog sources and retain the reviewed PURL and public-export corrections. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 3 +- docs/mxc-policy-store-api.md | 731 +++++++++++++++++++++++++ docs/mxc-policy-store.md | 1002 +++++----------------------------- 3 files changed, 882 insertions(+), 854 deletions(-) create mode 100644 docs/mxc-policy-store-api.md diff --git a/README.md b/README.md index 309f1254c..8acb81b35 100644 --- a/README.md +++ b/README.md @@ -316,7 +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.md](docs/mxc-policy-store.md) | MXC SDK Policy Store APIs and bundled best-effort policy data | +| [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 index 7d678d625..b1d0a9507 100644 --- a/docs/mxc-policy-store.md +++ b/docs/mxc-policy-store.md @@ -31,6 +31,8 @@ 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. @@ -79,35 +81,11 @@ checklist in time and ships in the existing MXC SDK packages. See [§6](#6-intended-repository-and-packaging-boundary). -Defaults and omission behavior are: - -- `ResolveContext` is optional and carries lookup context only. No command or - execution settings are required, inferred, or returned by the resolver. -- Existing callers do not perform catalog lookup automatically. A consumer - must explicitly enable or invoke it. -- Omitted `ResolveContext.platform` uses the current host platform. -- Omitted `ResolveContext.architecture` uses the device's native system - architecture, not the architecture of the library's process or a detected - tool build. Explicit caller selection takes precedence. See the selection - rules and emulation risk in [§4.4](#44-platform-variants). -- Omitted `ResolveContext.catalogRevision` uses the currently installed - catalog revision. -- Omitted `ResolveContext.allowWeakIdentityFallback` is `false`. -- Omitted `projectRoot` and `symbols` provide no caller overrides. The resolver - uses supported local discovery and shared documented defaults for required - symbols as specified in [§4.2](#42-entry-shape). It does not invent a project - root or an installation path. A selected entry with an unresolved required - symbol is not resolvable. -- The resolver does not fabricate an omitted `packageUrl` or `detectedVersion`. -- Omitted `ToolCandidate.detectedVersion` selects the unversioned default, - without a version warning. -- Omitted `ToolCandidate.intent` selects the base plus all intents of the - effective version policy. An unsupported intent contributes no policy and - produces an `intent_unsupported` warning. -- If no policy can be resolved, `resolveToolRequirements` yields `undefined`. - `resolveToolRequirementsWithDiagnostics` yields a result whose `requirements` is - `undefined`, preserving the diagnostics. The consumer's restrictive baseline - remains unchanged. + +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 @@ -391,808 +369,108 @@ 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. -For symbols required by selected entries and dependencies, precedence is: - -1. Explicit caller values from `projectRoot` or `symbols`, as applicable. -2. Supported local discovery, including `PATH`, known host locations, and - relevant tool configuration overrides. -3. A documented default for the selected platform, if applicable. -4. Unresolved, with diagnostics and no partial policy. - -Tool-specific discovery runs in the library, not in catalog-supplied code. -It does not execute candidate tools, install software, or contact the network. -Automatic discovery and host-derived values describe the current host and -environment; callers targeting another execution environment supply overrides. -A failed configuration read is an explicit library error, not evidence that -no override exists and a default should be used. Discovery does not verify -tool identity. The diagnostics operation reports the source and resolved value -of each discovered or defaulted symbol through `diagnostics.warnings`. +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 -Lookup uses tool identity, an optional detected version, and optional -tool-defined intent. Package URL -provides strong identity and invocation name is the opt-in fallback. Raw -command lines and calling-application identity are not lookup keys; the caller -maps operations to intents and applies its own restrictions. Platform and -architecture remain resolution context. - -`identity` describes the predicates a candidate can satisfy for an entry, -using the layering #779 §3.1 establishes (invocation name vs. launcher artifact -vs. executing image; falsifiable-against-a-local-artifact as the admission test -for a new kind). - -Caller-supplied identity is not verified identity. A `packageUrl` match does -not prove that the installed tool belongs to that package; the caller is -responsible for verifying that association. The library does not inspect the -tool to verify it. - -Parse candidate and catalog package URLs using the -[PURL component rules](https://github.com/package-url/purl-spec/blob/7cd2d3442fb9c88155db17ada7c911b40ec22d41/docs/specification/standard/Clause-5-Package-URL-Specification.md) -and applicable type definitions, including percent-decoding before comparison. -Compare only type, namespace, and name. Catalog comparison is -locale-independent and case-insensitive for type and namespace; the name -follows its package type's normalization and case rules. Namespace folding is -a catalog lookup rule, not a claim that every ecosystem treats namespaces -case-insensitively. - -Ignore candidate version, qualifiers, and subpath for matching and record -`purl_components_ignored` when any is present. Only `detectedVersion` supplies -tool-version evidence. Validate the complete PURL before ignoring components. -An invalid candidate PURL makes that pair `tool_unmatched` with `purl_invalid`; -do not repair it, retry invocation-name matching, or fail other pairs. -Invalid catalog PURLs are catalog validation errors. - -Invocation-name matching is locale-independent and case-insensitive on Windows -and macOS, and exact on Linux. This does not change the caller's command or -package-identity matching. - -For each input tool, consider entries with an eligible identity predicate and -their applicable platform additions. Rank matches in this order: - -1. Package URL match over invocation-name-only match. -2. A requested intent declared in the default or applicable overlays over no - matching intent declaration. -3. Exact architecture over architecture-neutral additions or the common default. - -Select the unique highest-ranked match. Two distinct matches tied after all -three comparisons produce an error, not a union or a file-order tie-break. -Multiple predicates satisfied within the same entry count as one identity -match, ranked by its strongest satisfied predicate. - -After selecting the entry, resolve its version and then its intent as below. -Version ranges do not choose a different tool entry. -Failure for that pair does not retry another entry or a wildcard entry. -Composition includes only contributing pairs in an array request. - -Composition applies across tools in an array request, not across competing -identity matches for one tool. Diagnostics follow input order and identify the -single selected entry, satisfied identity predicates, and selected intents. - -Invocation-name-only matching requires explicit opt-in -(`allowWeakIdentityFallback: true`). Intent selection does not bypass that -option. - -`versionRange` uses the Package-URL project's -[VERS syntax](https://github.com/package-url/vers-spec/blob/797c842a4afebf258e6710a68cd60306afc36708/docs/specification/standard/Clause-5-VERS-Specification.md): -`vers:/`, for example `vers:npm/>=10.0.0|<12.0.0`. -V1 supports these [version types](https://github.com/package-url/vers-spec/blob/797c842a4afebf258e6710a68cd60306afc36708/docs/types/vers-types.md): - -| VERS type | Version parsing and comparison | -|---|---| -| `npm` | node-semver version rules, as referenced by the VERS npm definition | -| `semver` | Semantic Versioning 2.0.0 | -| `pypi` | PEP 440 | -| `nuget` | NuGet version normalization and comparison | -| `intdot` | VERS dotted-integer comparison for numeric tool versions such as Git `2.40` | - -VERS defines the range syntax and interval evaluation; the named type defines -version parsing, normalization, and ordering, including prereleases and -accepted prefixes. Do not apply npm's native range syntax to other types or -strip version prefixes independently of the named rules. `generic` is not -supported while its upstream comparison algorithm is unspecified. - -Catalog authoring/build validation rejects malformed VERS strings, invalid -constraints, and unsupported types as invalid catalog data. - -For each requested tool/intent pair, parse `detectedVersion` under the selected -entry's `versionScheme`. Use only that scheme's specified parsing and -normalization; do not repair rejected input, try other schemes, or perform -fuzzy matching. - -| Version input | Effective policy data | Version status / warning | -|---|---|---| -| Omitted | Default plus platform additions | `matched_default`; no version warning | -| Valid, inside exactly one range | Default plus platform and selected version additions | `matched_version`; record the selected range | -| Valid, in no range | Default plus platform additions | `version_out_of_range` warning | -| Unparseable under the entry's scheme | None for this pair | `version_unparseable` warning | - -An out-of-range version never selects the nearest, highest, or broadest variant. -An unparseable version contributes nothing, while other requested pairs still -resolve. After version selection, a named intent must exist in the effective -policy. Otherwise emit `intent_unsupported` and contribute nothing for that -pair, not the base or an all-intents fallback. An out-of-range version requesting -a newer-only intent emits both warnings and contributes nothing. With no -intent, combine the effective base with all of its effective intents. - -No catalog match yields `tool_unmatched`, no contribution, and no wildcard -fallback. Diagnostics preserve input order. These per-pair outcomes are not -whole-request failures. +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. -### 4.4 Platform variants +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. -Supported platforms are `windows`, `linux`, and `macos`. Supported architecture -selectors are `x64` and `arm64`. A variant selector has this closed shape: +### 4.4 Platform variants -```ts -interface PlatformVariantSelector { - platform: "windows" | "linux" | "macos"; - architecture?: "x64" | "arm64"; -} -``` +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. -A platform variant contains only additions to the common default, using the -same `policyAdditions`, `dependencies`, `intentAdditions`, and `newIntents` -fields as a version overlay. It never removes, narrows, or replaces default -requirements. Select at most one platform/architecture overlay, then combine -its additions with the default and the selected version overlay. -Architecture is a catalog selector, not a field added to `ContainerRequest`. Omitting -`when.architecture` makes a catalog variant architecture-neutral; omitting the -caller's `ResolveContext.architecture` instead requests the host default. - -Selection first filters by platform. After identity and intent specificity -([§4.3](#43-identity)), architecture selection uses the following precedence: - -| Caller context | Preferred variant | Fallback | -|---|---|---| -| Explicit `architecture: "x64"` | x64 for the selected platform | Architecture-neutral for that platform | -| Explicit `architecture: "arm64"` | ARM64 for the selected platform | Architecture-neutral for that platform | -| Architecture omitted | Device's native system architecture for the selected platform | Architecture-neutral for that platform | - -The native system architecture is the architecture reported by the host OS, -not the architecture of the process hosting the library. For example, on an -ARM64 device with both x64 and ARM64 catalog variants and no neutral variant, -omitting architecture selects ARM64. An explicit `architecture: "x64"` selects -x64 on that same device. The resolver does not require a neutral variant to -return a result when the effective architecture has an exact match. - -Catalog validation rejects duplicate exact selectors and more than one -architecture-neutral variant for the same platform. If neither an exact nor -architecture-neutral variant exists, retain the common default without platform -additions; never select another architecture's overlay. A failure to determine -the native system architecture when it is needed is a library error, not a -guessed selection. - -**Emulation risk:** A host-derived default does not establish the architecture -of the installed tool. An x64 tool running under emulation on an ARM64 device -may need the x64 variant rather than the default ARM64 variant. The resolver -does not inspect or run the tool to discover its architecture. Callers that -know the relevant tool and runtime requirements should select architecture -explicitly and remain responsible for deciding whether the result applies. -Neither explicit selection nor a host default guarantees that the returned -policy is sufficient or minimal. Host-derived selection and neutral fallback -are surfaced through `diagnostics.warnings` by the diagnostics API -([§5.1](#51-runtime-lookup)). - -A platform variant must not name a specific MXC containment backend. Policies -stay backend-neutral; the selected backend still decides whether a stated -requirement can be realized on that host. +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 -Dependencies reference another entry's `entryId` and can belong to the base or -to an intent, in the default or a selected overlay. Include base dependencies -and only the selected intent dependencies. A dependency contributes only its -unversioned default base plus its applicable platform overlay's base -additions; it never selects a version overlay or includes intents. This -differs from a requested tool with no intent, which includes all effective -intents. A reference may name dependency intents, for example -`{ "entryId": "tool:ssh", "intents": ["connect"] }`; those intents, including -their applicable platform `intentAdditions`, are then added. Catalog validation -rejects a named dependency intent that is missing from any materialized -platform combination where the reference applies. An optional dependency -`versionRange` uses the same VERS syntax and authoring/build validation; -a range is not a detected version and does not select a version overlay. -Resolution is otherwise transitive, cycle-rejecting, and deterministic, and -the diagnostics API returns the resolved dependency metadata alongside the -policy. - -Compose each selected base with its selected intent additions, then combine -the results for different requested tools and their transitive dependencies. -De-duplicate each source contribution layer independently within an entry and -catalog revision. The default base contributes once, and each selected platform -base layer contributes once, regardless of requested version or intent. -Default intent additions are keyed by intent; platform intent additions by -platform selector and intent; version base additions by range; and version -intent additions by range and intent. Include new-intent definitions under -their owning overlay. Never attach the requesting pair's version or intent to -a shared base-layer key. Repeated identical layers contribute once; distinct -version and intent additions selected by different pairs are retained. -Preserve per-input attribution. A shared `ResolveContext` applies to the whole -lookup; each tool candidate carries its own intent. - -Following #779's floor semantics, filesystem composition preserves the access -required by all selected tools rather than intersecting their requirements. -An overlapping read-only requirement must not suppress another tool's needed -write access, and a catalog-provided deny must not block a required read or -write path. This is not a rule for merging consumer authorization policy. -Filesystem composition uses the exact -`filesystem.deniedPaths`, `filesystem.readonlyPaths`, and -`filesystem.readwritePaths` fields: - -1. All selected entries and additions use the containing catalog revision's - validated SDK target; no stored request contains a `version`. -2. At lookup time, resolve all required symbols and normalize paths using the - selected platform's path rules before comparing equal or - ancestor/descendant paths. Combine the selected entries and dependencies, - de-duplicating equivalent pathnames within each access class, not distinct - required alias locations. -3. Preserve every required read-write subtree. Remove read-only entries equal - to or contained within a read-write subtree, since read-write already - satisfies their read requirement. Retain a read-only ancestor of a - read-write subtree without promoting the whole ancestor to read-write. -4. Remove each catalog-provided deny that overlaps any required read-only or - read-write path, whether equal, an ancestor, or a descendant. Remove the - entire deny entry, not an invented exception beneath it. Retain - non-overlapping denies. -5. Return the composed policy and make the access changes available through - diagnostics. The same rules apply to overlaps within one selected entry - and across multiple entries; matching or traversal order must not change - the effective access. - -Filesystem comparison is separate from invocation-name matching. Equality, -de-duplication, and ancestor checks honor the applicable filesystem and -directory case-sensitivity, not a blanket OS assumption. When that information -cannot be determined, compare case-sensitively, preserve differently cased -paths for comparison, and report the assumption in diagnostics. Returned paths -retain their casing; comparison must not lowercase the policy paths. Another -target environment must not inherit this host's filesystem case rules. - -Resolve actual filesystem object identity using MXC's -[object comparison primitives](https://github.com/microsoft/mxc/blob/894f4c159705f5f470727e4fa1e363a2abec88f1/src/mxc-sdk/src/core/mxc_common/filesystem_object.rs#L100-L265). -Symlink, junction, hard-link, bind-mount, and 8.3 aliases must participate in -the same catalog access composition, not silently cause a required read-write -path to become read-only or denied when MXC later normalizes the request. -Keep this floor composition separate from the runner's restrictive enforcement. - -Object identity reconciles access, not pathname reachability. Retain every -required alias location even when multiple paths name the same object. If a -read-only alias names an object required read-write through another path, -retain that alias with the composed read-write access instead of deleting it. -Do not collapse same-class aliases merely because their object identities -match: path-based backends still need each required mount or pathname. - -If necessary identity cannot be established for the target, that pair -contributes nothing and reports `filesystem_identity_unresolved`; other -independently resolved pairs can still contribute. This includes unresolved -aliases introduced by dependencies or cross-pair composition. Do not treat -unknown identity as proof that paths differ, bypass this check with lexical -case rules, or weaken MXC enforcement. - -V1 inspects local host-side source paths, not a remote or guest filesystem. -Windows identity uses volume serial number and file ID through -`CreateFileW`/`FileIdInfo`; Unix uses `stat` device/inode (following links). -Compare opened/resolved objects, preserve every required alias pathname, and -compare existing ancestor identities for subtree overlap. A cleanly missing -suffix is compared relative to its deepest resolvable existing ancestor; it -is not treated as an existing alias. An unreadable component, broken link -whose target cannot be established, or different target filesystem is unknown -and drops every input whose access relationship depends on that fact. Never -drop only the restrictive side of an unresolved relation to retain its grant. -Case-sensitive comparison is only a lexical aid when case sensitivity is -unknown, not permission to skip object checks. Apply the same analysis across -pair/dependency boundaries. Discard all layers solely owned by excluded pairs, -then compose the remaining complete pairs; retain shared layers only for -remaining owners. No lookup result bypasses the runner's authoritative -enforcement-time checks or claims protection from later filesystem changes. - -| Resolved requirements | Composed filesystem policy | -|---|---| -| Read-only `/work` and read-write `/work` | Read-write `/work`; omit read-only `/work` | -| Read-write `/work` and read-only `/work/tools` | Read-write `/work`; omit read-only `/work/tools` | -| Read-only `/work` and read-write `/work/cache` | Retain read-only `/work` and read-write `/work/cache`; do not make all of `/work` writable | -| Denied `/data` and read-write `/data/cache` | Remove denied `/data`; retain read-write `/data/cache` and report that the entire `/data` deny was removed | -| Denied `/secrets` and read-write `/work` | Retain both non-overlapping entries | - -These are composition rules for the returned `ContainerRequirements`, not changes -to MXC's enforcement precedence. Simply concatenating a conflicting deny or -read-only entry with a grant is insufficient: the restrictive entry could -still prevent the access the composed floor is intended to request. - -Removing a parent deny removes its protection for the entire subtree, not -just the overlapping required path. It does not itself add a grant to that -subtree, but other grants can now apply there. Diagnostics must identify the -removed deny and this broader effect. Caller-owned denies and other user, -enterprise, device, or backend restrictions are never inputs to this -least-restrictive catalog composition and must not be removed by it. - -Publication checks validate policy shapes, symbols, and supported composition -fields and exercise the rules with known paths and fixtures. Equal or nested -filesystem requirements are not by themselves invalid catalog data. Caller -symbol values can introduce additional overlaps, so the resolver must always -apply these rules after substitution and normalization at lookup time. -An overlap covered by these rules is not a composition error; missing required -symbols or unsupported composed fields retain their existing failure behavior. - -Network requirements also combine across selected tools, intents, and -dependencies. A tool that needs no network contributes no network access; it -does not veto access required by another requested tool. For example, Git's -`local` intent alone needs no network, while a request combining it with a -tool needing HTTPS access includes that tool's HTTPS requirement. - -When only one selected component requires network, preserve its supported -network requirements; components with no grants do not force an additional -network merge. For multiple scoped outbound requirements in v1, retain -`network.egress.default: "deny"` and union the selected -`network.egress.allow` and catalog `network.egress.deny` rules, -de-duplicating identical rules. -Preserve each whole rule's destination, exclusions, protocol, and port -relationships; never form a cross-product of destinations and ports. Missing -network fields or deny-by-default contribute no grants, not a restriction on -another component's required grants. Do not introduce unrestricted outbound -access or include unselected version or intent additions. An omitted intent -selects all intents in the effective version policy; an unsupported intent -contributes no access. - -Catalog egress denies follow the filesystem deny rule. Remove each catalog -deny rule that overlaps any required allow rule, meaning their destination -CIDRs intersect after `except` exclusions and their protocol/port selectors -intersect. Remove the entire deny rule, not an invented exception within it. -Retain non-overlapping deny rules. The same rule applies within one entry and -across entries, and a conflicting deny never fails the request. -Diagnostics identify the removed rule, its full destination and port scope, -and the contributing entries. - -This combined access belongs to the shared sandbox, not to isolated -permissions per tool. Caller-owned network denies and other user, enterprise, -device, or backend restrictions are never inputs to this composition and are -never removed by it. Other network configuration, including allow-by-default, -non-default ingress, and proxy configuration, is rejected rather than -approximated with broader access when more than one selected policy sets it. -The same rejection -applies to timeout, clipboard, lifecycle, UI, and every other field without an -explicit composition rule. A single selected policy without additions or -dependencies may use catalog-supported fields without cross-policy 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 MXC SDK APIs separate runtime resolution from catalog inspection. -Resolution accepts one tool or an array, selects the most specific match for -each input, and composes its effective base, selected intents, and dependencies into one -`ContainerRequirements`. Callers choose a requirements-only operation or a diagnostic -operation over the same resolution logic. Neither -implicitly returns the whole catalog. These are SDK library calls, not a -hosted service or a command-line utility. - -The signatures below use TypeScript to describe the shared contract. Rust and -C# expose the same operations and metadata with idiomatic names and types. -TypeScript and C# expose single-tool and array overloads; Rust uses an idiomatic -one-or-many input type because it does not support function overloading. An -absent policy is `undefined` in TypeScript/JavaScript, `None` in Rust, and -`null` in C#. Library failures remain distinct from policy absence. +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 -`ContainerRequirements` preserves the earlier four-field scope: filesystem, -network, UI, and timeout, with the composition rules in §4.5. It reuses the -SDK's nested types and optionality; `Pick` is not runtime validation or an -expansion of the catalog's supported fields (§4.2). Rust/.NET use an equivalent -four-field aggregate, not duplicate nested models. - -Context is optional and lookup-only. Command, containment, name, working -directory, environment, cleanup, proxy, and operation options remain caller-owned, -not resolver inputs or outputs. Intent selects requirements, not a command. -Resolve again or supply overrides if the eventual execution environment differs -from discovery. Requirements do not certify coverage of an arbitrary command. - -Node resolution uses Promise-returning plain verbs, matching the v1 -[run/spawn convention](https://github.com/microsoft/mxc/blob/894f4c159705f5f470727e4fa1e363a2abec88f1/sdk/node/src/v1/container.ts#L428-L477); -filesystem work must not block the event loop. Rust exposes -`v1::resolve_tool_requirements` / `resolve_tool_requirements_with_diagnostics` -as `Result, Error>` / `Result`. -.NET exposes `MxcContainer.ResolveToolRequirements` and -`ResolveToolRequirementsWithDiagnostics` (plus `Async` Task forms) in `V1`. -Metadata inspection stays synchronous; no operation creates a container. - -Failures reuse existing MXC error codes, which are sufficient for normal -programmatic handling. An optional `details.reason` may provide a stable, -catalog-specific distinction for logging, investigation, or finer handling -when the code alone is too broad. Callers need not branch on it; an absent or -unrecognized reason retains the same handling as the primary code. A reason -must not duplicate a distinction already expressed by an existing MXC code. - -The following primary-code mappings apply across all language bindings. -When `details.reason` is supplied for these failures, it uses the listed value; -callers may ignore it. - -| Failure | MXC error code | Optional `details.reason` | -|---|---|---| -| Invalid tool input or resolution context | `malformed_request` | `invalid_context` | -| Invalid catalog data, including invalid dependency references or cycles | `policy_validation` | `invalid_catalog` | -| Distinct matches for one tool tied at the highest identity/intent/architecture rank | `policy_validation` | `ambiguous_match` | -| Unsupported composition, including incompatible SDK target metadata or fields without a composition rule | `policy_validation` | `composition_conflict` | -| Unsupported or undetectable host platform or architecture | `unsupported_containment` | `unsupported_host` | -| Bundled catalog content cannot be read | `backend_error` | `integrity` | -| Explicitly requested catalog revision is not installed | `backend_error` | `revision_unavailable` | - -Filesystem and network overlaps handled by -[§4.5](#45-dependencies-and-composition) are not composition failures. Ordinary no-match results remain policy absence, not an -error from this table. Invalid candidate PURLs, well-typed but unparseable -version strings, unsupported intents, and unresolved filesystem identity are -per-pair diagnostic outcomes, not whole-request errors from this table. - -```ts -import type { ContainerRequest, NetworkRuleConfig } from "@microsoft/mxc-sdk/v1"; - -export type ContainerRequirements = Pick; - -interface ToolCandidate { - invocationName: string; - packageUrl?: string; - detectedVersion?: string; - intent?: string; -} - -type ToolInput = string | ToolCandidate; - -interface ResolveContext { - projectRoot?: string; - symbols?: Record; - platform?: "windows" | "linux" | "macos"; - architecture?: "x64" | "arm64"; - catalogRevision?: string; - allowWeakIdentityFallback?: boolean; -} - -interface IntentSelection { - requested?: string; - mode: "named" | "all" | "none" | "unsupported"; - selected: string[]; -} - -type VersionStatus = - | "matched_default" - | "matched_version" - | "version_out_of_range" - | "version_unparseable"; - -interface VersionSelection { - status: VersionStatus; - detectedVersion?: string; - selectedVersionRange?: string; -} - -type ToolResolutionStatus = - | VersionStatus - | "intent_unsupported" - | "tool_unmatched" - | "filesystem_identity_unresolved"; - -type InputWarning = { inputIndex: number; message: string }; -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 } -); - -interface WarningScope { - inputIndexes: number[]; - entryIds: string[]; - message: string; -} - -interface PathRequirement { - path: string; - access: "denied" | "readonly" | "readwrite"; - entryIds: string[]; -} - -type EgressRule = - NetworkRuleConfig; - -interface NetworkRequirement { - rule: EgressRule; - entryIds: string[]; -} - -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[] } -); - -type PolicyResolutionWarning = ToolResolutionWarning | ResolutionDetailWarning; - -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[]; - }; -} - -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; -``` - -A string input is shorthand for `{ invocationName: tool }`; it supplies no -intent, package, or version evidence and follows the same weak-identity option -as an object input. For example, name-only lookup under the opt-in -rule is: - -```ts -const ctx: ResolveContext = { - 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`, - programData: String.raw`C:\ProgramData`, - temp_dir: String.raw`D:\temp`, - }, -}; -const push = await resolveToolRequirementsWithDiagnostics( - { invocationName: "git", detectedVersion: "2.45", intent: "push" }, ctx); -const bundle = await resolveToolRequirementsWithDiagnostics( - { invocationName: "git", detectedVersion: "2.55", intent: "bundle-fetch" }, ctx); -const noVersionBundle = await resolveToolRequirementsWithDiagnostics( - { invocationName: "git", intent: "bundle-fetch" }, ctx); -const combinedRequirements = await resolveToolRequirements( - [{ invocationName: "git", detectedVersion: "2.45", intent: "push" }, "node"], ctx); -for (const warning of push.diagnostics.warnings) { - if (warning.code === "filesystem_deny_removed") { - console.log(warning.removed.path, warning.requiredBy, warning.entryIds); - } -} -``` - -Later, reviewed and constrained requirements combine with a command without -converting their nested types: - -```ts -function createRequest( - approvedRequirements: ContainerRequirements, - command: string, -): ContainerRequest { - return { ...approvedRequirements, command }; -} -``` - -For the Git entry above, with referenced dependencies available: - -| Request | Selected version data | Result | -|---|---|---| -| `2.45` + `push` | Default + Windows additions + `vers:intdot/>=2.40\|<2.50` | `matched_version`; push policy plus the SSH dependency's default and Windows base additions | -| `2.55` + `bundle-fetch` | Default + Windows additions + `vers:intdot/>=2.50\|<3` | `matched_version`; new bundle-fetch policy, no inherited SSH addition | -| No version + `bundle-fetch` | Default + Windows additions | `intent_unsupported`; `requirements` is `undefined` for this single-pair call | - -Single-tool lookup is equivalent to a one-element array; its diagnostic -`inputIndex` is `0`. A caller retaining separate policies per tool can use -single-tool calls. A caller wanting one sandbox for several tools passes an -array. Both forms select one most-specific match per input, then compose the -contributing requirements. A string input uses the unversioned default and all -its effective intents, including applicable platform additions. - -Both operations yield the same requirements; the diagnostics form adds -attribution and warnings from that resolution pass. No second lookup or -process-global "last result" state is needed. - -**The returned requirements may cover only a subset of the requested tools.** -Partial results are intentional in both APIs. `tool_unmatched`, -`version_unparseable`, `intent_unsupported`, and -`filesystem_identity_unresolved` pairs contribute nothing; other pairs still -resolve. The requirements-only call makes no coverage promise, even when its result -is non-`undefined`. Callers needing to know which pairs contributed use -`resolveToolRequirementsWithDiagnostics` and inspect per-input statuses. -No wildcard entry fills a missing match, and there is no `requireAllMatches` -option. - -Each input has a diagnostic record in input order. A `tool_unmatched` input -has an empty `matches` list and a warning. An empty array or a lookup with no -contributing pairs produces no requirements, not an empty requirements object: -`resolveToolRequirements` yields `undefined`, while the diagnostics operation yields -a `ToolRequirementsResolution` with `requirements: undefined`. An empty array has no -per-input records. Unresolved required symbols in selected entries prevent a -policy from being returned and produce diagnostics; they are not grounds for -silently omitting a selected requirement to produce a partial policy. - -Each input's `matches` contains at most one identity-matched entry, including -when its version or intent prevents contribution. Equally specific matches -remain an ambiguity error. `versionSelection` reports the supplied version, -version status, and selected range only for `matched_version`. - -`intentSelection` reports `mode: "named"` or `"all"` and sorted selected names. -An unsupported name uses `"unsupported"` and an empty list. Version parse -failure skips intent resolution. With no intent and no effective intents, -`"all"` has an empty list and the effective base still contributes. - -A contributing pair's status is its version status. Unresolved target object -identity makes its status `filesystem_identity_unresolved` while preserving -any completed version and intent selection. Unsupported intent makes -the pair's status `intent_unsupported` while retaining the version status in -`versionSelection`. Structured warnings preserve input order; for an -out-of-range version and unsupported intent, emit `version_out_of_range` then -`intent_unsupported`. All warnings are structured records; `code` selects the -category's fields and `message` is human-readable, not a parsing contract. -These statuses never silently select another variant or entry. - -Per-input warning fields retain the prototype's `inputIndex`, `entryId`, -`detectedVersion`, `intent`, and `message` vocabulary where applicable. -Warnings about shared contributions use sorted, distinct `inputIndexes` and -`entryIds`; each path/rule contribution also identifies its source entries. -`removed` always contains the complete original deny path or egress rule, -including exclusions and protocol/port selectors, not merely the intersection. -Removing it can affect its entire scope wherever other grants apply. - -Dependency metadata includes version and intent selections. A dependency -always reports `matched_default`, and `mode: "none"` with an empty list unless -its reference names intents, which report `"named"`. De-duplicate identical -entry/revision/range/selection records and sort by those fields. Shared -contributions retain sorted, distinct `inputIndexes` for every contributing -requester, including transitive dependencies. Union these indexes when -de-duplicating identical records; requester indexes are not part of the -dependency-record identity. - -When architecture is omitted, diagnostics include a warning naming the -effective native system architecture and stating that the tool's architecture -was not verified. Architecture-neutral fallback is also identified. These -diagnostics describe selection; they do not attest to the installed tool's -architecture. The requirements-only operation does not expose warnings or -attribution; consumers needing them use `resolveToolRequirementsWithDiagnostics`. - -Composition diagnostics report read-only requirements superseded by -read-write requirements, and catalog filesystem and egress denies removed to -satisfy required access. Each warning identifies the resolved paths or rules, -access classes, and contributing entry IDs. A removed deny warning names the -full removed scope and explains that other grants may now apply throughout -it, not only at the overlap. Both APIs return the same composed policy; -callers needing to review these adjustments use -`resolveToolRequirementsWithDiagnostics`. +`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 -```ts -type CatalogPlatform = "windows" | "linux" | "macos"; -type CatalogArchitecture = "x64" | "arm64"; - -type CatalogIdentityMetadata = - | { kind: "purl"; value: string } - | { kind: "invocation-name"; names: string[] }; - -interface CatalogIntentMetadata { - name: string; - exampleSubcommands?: string[]; - dependencyEntryIds: string[]; -} - -interface CatalogAdditionsMetadata { - dependencyEntryIds: string[]; - intentAdditions: CatalogIntentMetadata[]; - newIntents: CatalogIntentMetadata[]; -} - -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; - }; -} - -export declare function listCatalogEntries(): CatalogEntryMetadata[]; -export declare function getCatalogInfo(): { - catalogSchemaVersion: string; - catalogRevision: string; - sdkContractVersion: string; -}; -``` - -This supports setup UI, catalog browsing, and update decisions without paying -the cost of policy resolution, and keeps "give me everything" out of the -runtime lookup path entirely. Metadata exposes the default and overlay -selectors, inherited/new intent names, subcommand hints, dependency IDs, and -provenance, but not an unresolved or resolved policy body. +`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 -A consumer that uses this API: - -1. Decides whether automatic lookup is enabled at all. -2. When persisting an accepted policy, retains its `catalogRevision` and - contributing entry IDs/revisions from diagnostics, whether the policy - covers one tool or several. -3. Keeps catalog-derived requirements in a layer separate from its own user, - learned, and invocation-specific policy. -4. Applies its own authorization, elevation, and restrictive-composition - rules on top. -5. Enforces its OS, enterprise, device, and backend ceilings regardless of - what the catalog returned. -6. Fails closed when a required entry cannot be realized on the current - host/backend. It falls back to its own restrictive baseline and does not - run uncontained. -7. Uses the diagnostics operation when attribution or audit is needed, and - records matched identities, catalog/entry revisions, warnings, and approval - state in its own audit trail. - -The catalog APIs never write a consumer's policy store. A consumer's own -capability observation (see [§8](#8-relationship-to-learning-mode)) can produce -candidate evidence for a future contribution to this catalog; it is not a -mechanism for mutating the catalog at request time. +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 @@ -1205,48 +483,17 @@ 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 - -The MXC SDKs expose requirements using their v1 request field types: - -| Language | Existing MXC SDK | Requirements type | -|---|---|---| -| TypeScript / JavaScript | `@microsoft/mxc-sdk/v1` | `ContainerRequirements` (`Pick`) | -| Rust | `mxc-sdk` | `mxc_sdk::v1::ContainerRequirements`, using v1 section types | -| C# / .NET | `Microsoft.Mxc.Sdk.V1` | `ContainerRequirements`, using v1 section types | - -Returned requirements use the containing SDK's supported fields. Unsupported -data must not be silently dropped to fit its types. - -A consumer: - -1. Installs an MXC SDK release with its bundled catalog. -2. Inspects the catalog or resolves requirements using §5's APIs. -3. Reviews access and coverage, preserving its restrictive baseline on absence - and applying [§5.3](#53-consumer-obligations). -4. Supplies command/execution settings later, applies its restrictions, and - passes the resulting `ContainerRequest` to MXC. -The SDK API reference and repository/package READMEs must state: - -> Returns a best-effort MXC policy baseline for representative tool workflows, -> not authorization or a guarantee of success or safety. It may request broader -> access. Callers and users review that access and may further constrain or -> override the recommendation; enterprise and device restrictions remain -> authoritative. Diagnostics explain the contributing requirements and changes. -> A returned policy may cover only a subset of requested tools; inspect -> per-input statuses to determine coverage. +### 6.1 Library distribution and consumption -Lookup is local and does not download updates, contact a hosted service, or -run the candidate tool. `ResolveContext.catalogRevision` selects a revision -included in the installed SDK; an unavailable revision is an error, not a -request to download or substitute data. An omitted revision uses the bundled -default. +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 catalog updates ship with an MXC release. Revision metadata can identify the -bundled data independently of the SDK package version without implying a -separate artifact delivery channel. Installing an SDK update does not rewrite -a consumer's previously accepted policies. +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 @@ -1274,6 +521,50 @@ 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`. @@ -1398,10 +689,12 @@ separate catalog digest or runtime checksum. Schema validation still applies. 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 primary-code mappings in [§5.1](#51-runtime-lookup); +- 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 @@ -1461,8 +754,9 @@ separate catalog digest or runtime checksum. Schema validation still applies. 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 and namespace compare case-insensitively, names follow their - type's rules, and equivalent percent-encodings compare after parsing +- 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 @@ -1479,7 +773,7 @@ separate catalog digest or runtime checksum. Schema validation still applies. - 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 - diagnostics specified in [§5.1](#51-runtime-lookup); host-architecture + [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)): @@ -1527,6 +821,8 @@ separate catalog digest or runtime checksum. Schema validation still applies. - 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 @@ -1580,7 +876,7 @@ separate catalog digest or runtime checksum. Schema validation still applies. | Maintainer sign-off | Recommended answer | |---|---| -| Approve the command-free requirements API surface? | `resolveToolRequirements` / `resolveToolRequirementsWithDiagnostics` return `ContainerRequirements` using existing v1 section types, with optional lookup context, Promise-based Node resolution, corresponding Rust/.NET bindings, and the documented partial-result contract. | +| 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