Spec: Filesystem Profile Resolution
Mirrored from
docs/design/policy-sandbox/specs/fs-profile-resolution.md. Edit the source document in the repository, not this generated page.
PolicyDef::effective_profile and PolicyDef::check_path are the load-bearing functions for every filesystem allow/deny decision in Orbit. This spec names the invariants those functions must preserve and the failure modes callers must handle.
Why This Exists
Section titled “Why This Exists”The resolution algorithm has multiple layered transformations (lookup, normalization, deny injection, last-match-wins evaluation) and a special-case fallback for the implicit unrestricted profile. Without a prescriptive spec, future changes to any one layer can break a property that another layer relies on.
Resolution Invariants
Section titled “Resolution Invariants”- Schema acceptance. Only
schemaVersion: 2policies are accepted. v1 is rejected at load time with an explicit migration message that namesspec.denyRead,spec.denyModify, andspec.fsProfiles. - Profile lookup.
effective_profile(profile_name)returns the named profile if present. If absent andprofile_name == "unrestricted", it synthesizesFsProfile { read: ["./**"], modify: ["./**"] }. Any other absent name returnsOrbitError::InvalidInput. - Rule normalization. Every rule is trimmed, has backslashes converted to forward slashes, has leading
./stripped, and is rejected if it contains~,~/, parent traversals, or absolute paths. The normalizer also compiles the rule to its glob-equivalent regex; a rule that fails to compile is rejected at load. - Deny injection. Every entry of
denyReadis appended to the resolved profile’sreadlist as!<rule>; every entry ofdenyModifyis appended to the resolved profile’smodifylist as!<rule>. Injection happens after profile lookup so the implicitunrestrictedprofile is also subject to global denies. - Validation invariants.
- Profile names are non-empty.
- A positive
modifyrule is covered by at least one positivereadrule in the same profile (rule_covers_path_rule). - A profile rule that exactly equals a global
denyReadordenyModifyentry is rejected. denyReadrules are also treated asdenyModifyfor the validation cross-check: a profile cannot grant modify on a path that is globally read-denied.
- Merge contract.
PolicyDef::merged(global, workspace)overrides globalfsProfilesby name with workspace entries, accumulates globaldenyRead/denyModifywith workspace additions (deduplicated), prefers the workspace description when set, and re-runsvalidateon the merged result.
Evaluation Invariants
Section titled “Evaluation Invariants”- Path normalization. Caller-supplied paths are normalized via
normalize_path: trim, slash-flip, strip leading./. Absolute paths and~-anchored paths are rejected. - Empty rule list. If the operation’s rule list is empty after deny injection, the decision is
allowed = falsewithmatched_rule = "[]". - Rule walk. The evaluator walks the rule list in order and tracks the most recent match. The decision uses the last match’s negation flag: positive match → allow, negated match → deny.
- Empty positive set. If the rule list contains no positive rules (only negated rules), the decision is
allowed = falsewithmatched_rule = "[]". - No matching rule. If positive rules exist but none match, the decision is
allowed = falsewithmatched_rule = "<no matching rule>". - Matched-rule reporting. A positive match reports the original rule string in
matched_rule. A negated match reports the inner pattern (without the leading!) and surfaces asallowed = false. There is no separately persisted negation flag onFsCheckResult,FsPolicyEvaluation, orFsCallEvent— the only structural signal that a match was a deny is theallowed = falsevalue. Audit consumers that need to distinguish “denied by an explicit deny rule” from “denied because no rule matched” must inspectmatched_ruleagainst the policy’s deny lists themselves.
Glob Translator
Section titled “Glob Translator”- Supported syntax:
*(single-segment wildcard, anchored to[^/]*),**(cross-segment wildcard, anchored to.*),**/segment (anchored to(?:.*/)?),?(single character within a segment, anchored to[^/]),<prefix>/**directory-subtree match (anchored to^<prefix>(?:/.*)?$). - Unsupported syntax: character classes (
[abc]), brace expansion ({a,b}), POSIX bracket expressions, leading**/followed by another**, escape sequences. Rules that need these will hit translator gaps before they hit the evaluator. - Anchoring. Compiled regexes are anchored at both ends (
^…$). Partial matches do not satisfy a rule.
Failure Modes
Section titled “Failure Modes”- Profile missing.
effective_profile("unknown")(where the policy does not defineunknownand the name is notunrestricted) returnsOrbitError::InvalidInput. Callers must treat this as a configuration error, not a deny. - Rule normalization failure. A rule that escapes the workspace, is empty, or fails to compile to a regex returns
OrbitError::InvalidInputat validation or resolution time. Loaders must surface this to the user; runtimes must treat it as a stop-the-world error rather than falling back to deny-all. - Workspace canonicalization failure. The tool layer’s
workspace_relative_pathfalls back to the non-canonical workspace root whencanonicalizefails. A path that cannot be expressed workspace-relative surfaces asOrbitError::PolicyDenied("path is outside workspace"), which is conservative but does not distinguish “workspace deleted” from “path actually outside.” - Empty
readrule list. A profile authored without read rules denies every read withmatched_rule = "[]". This is almost always a misconfiguration but is treated as a valid (if useless) profile.
Migration Rules
Section titled “Migration Rules”- New profile fields must extend
FsProfileandResolvedFsProfiletogether; the resolver and the validator both consumeResolvedFsProfile. - New deny categories (e.g.,
denyExec) must be injected as negated rules into a corresponding rule list rather than evaluated as a separate pass; this preserves the single-walk evaluation contract. - Schema version bumps must reject the previous version explicitly at load and name the migration in the error message, the same way v1 → v2 currently does.
Agent Signature
Section titled “Agent Signature”Last revised by claude / claude-opus-4-7 for [T20260426-0622].