UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

85 lines • 4.21 kB
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import { fromUnits } from "../../rs/document.js"; import { admissionMatcher } from "../../rs/policy.js"; import { Outcome, } from "../types.js"; import { commandFacts } from "./rule.js"; /** * Where in the document a rule was written, as a host reads it: the * mount section it belongs to, or the top level. */ export function sourceOf(rule) { return rule.mount ? `mounts.${rule.mount}` : 'top'; } /** * The profile's answer about one line: the whole law, in one place. * * Two rules, because a command name and a path are not the same kind of * thing. A rule naming no path is read by verb, deny before ask, * wherever it was written: it is off the path axis entirely, so one in a * mount section scores 0 exactly as a top-level one does. Writing it * under `mounts./repo` scopes it to lines working inside that mount * (the rule reads `rule.mount`); it does not make it more specific * than a rule about the whole session. That is what keeps "denied * generally, asked inside one mount" inexpressible for a pathless rule, * which in practice means an account CLI: such a CLI reaches a service * and touches no mount, so scoping it to one was never meaningful. * * A rule carrying paths is read by anchor depth, the deeper entry * winning, ties broken by verb. The depth is the matched entry's, not * the rule's deepest, so an entry that says nothing about this operand * cannot lend it specificity. The allow list is asked first, since a * line no list covers never reaches a rule. * * All of which is settled *per subject*, and only then across them, * because a line names more than one path and a carve-out * written for one of them must not answer for the rest: with `deny cp * /protected/*` and a deeper `ask cp /review/deep/*`, `cp * /protected/secret /review/deep/out` is the source's deny, and reading * one best match for the whole line answered it with the destination's * ask instead, so a nod meant for the destination carried the protected * file out. * * Ranking across subjects is the whole answer for a deny, which refuses * the line, and only half of it for an ask, which is a question the host * still has to answer. So every ask that won a subject of its own is * reported (`Ruling.asks`) and the entry point requires all of them: with * `ask cp /a/*` and a deeper `ask cp /deep/b/*`, `cp /a/x /deep/b/y` used * to present the deeper one alone, and a nod for the destination ran the * line without the source ever being asked about. * * `PermissionsPolicy` renders this into the outcome table and `explain` * reports it, so the two cannot disagree about what a line would do. */ export function decide(ctx, rules) { if (rules === null) return { outcome: Outcome.ALLOW, rule: null, matchedPath: null, source: '', asks: [] }; const answer = admissionMatcher(rules).ask('decide', { facts: commandFacts(ctx) }); const listed = [...rules.deny, ...rules.ask]; const ruleAt = (id) => { const rule = listed[id]; if (rule === undefined) throw new Error('Invalid policy rule reply'); return rule; }; const rule = answer.rule === null ? null : ruleAt(answer.rule); return { outcome: answer.outcome, rule, matchedPath: answer.matched_path === null ? null : fromUnits(answer.matched_path), source: answer.allow_list ? 'commands.allow' : rule === null ? '' : sourceOf(rule), asks: answer.asks.map(ruleAt), }; } //# sourceMappingURL=decide.js.map