Initial Commit
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
export {
|
||||
compileFetchRule,
|
||||
compileFsRule,
|
||||
globToRegex,
|
||||
normalisePath,
|
||||
PermissionPolicy,
|
||||
POLICY_MANIFEST_SCHEMA,
|
||||
} from "./policy.js";
|
||||
export type {
|
||||
CompiledFetchRule,
|
||||
CompiledFsRule,
|
||||
PolicyManifestView,
|
||||
ResolvedBudgets,
|
||||
} from "./policy.types.js";
|
||||
@@ -0,0 +1,127 @@
|
||||
# `policy` — `PermissionPolicy`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module owns the construction-time value object that holds the
|
||||
compiled allowlists derived from a `PermissionsManifest`. Building the
|
||||
policy is the only place where untrusted manifest input crosses a trust
|
||||
boundary in this package — `Zod` validation runs once here, and every
|
||||
subsequent `PermissionChecker` call is O(rules) with no further string
|
||||
parsing.
|
||||
|
||||
The policy is intentionally immutable. Hot-reload constructs a fresh
|
||||
`PermissionPolicy` and the sandbox host swaps the reference atomically;
|
||||
checker invocations in flight against the old policy keep using the old
|
||||
allowlist, so a deny / allow decision is always consistent within a
|
||||
single request.
|
||||
|
||||
## Features
|
||||
|
||||
- `PermissionPolicy` — compiled value object exposing
|
||||
`fs[]`, `fetch[]`, `budgets`, and the original `manifest` for
|
||||
capability gating.
|
||||
- `POLICY_MANIFEST_SCHEMA` — Zod schema used at the ingestion boundary.
|
||||
- `compileFsRule(rule)` — converts a `permissions.fs[]` entry into a
|
||||
`CompiledFsRule` with `write` flag and resolved root.
|
||||
- `compileFetchRule(rule)` — converts a `permissions.fetch[]` entry
|
||||
into a `CompiledFetchRule` supporting exact host and `*.suffix`
|
||||
matching plus optional `http:` prefix.
|
||||
- `normalisePath(input)` — resolves `..`, strips trailing slashes,
|
||||
always returns POSIX form.
|
||||
- `globToRegex(pattern)` — minimal glob → regex translation
|
||||
(`*`, `**`, `?`, `[abc]`).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { PermissionPolicy } from "@suaveplan/api-sandbox-permissions/policy";
|
||||
|
||||
const policy = new PermissionPolicy({
|
||||
permissions: {
|
||||
fs: ["./uploads/*"],
|
||||
fetch: ["api.stripe.com"],
|
||||
budgets: { wallClockMs: 30_000 },
|
||||
},
|
||||
});
|
||||
|
||||
console.log(policy.budgets.wallClockMs); // 30000
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
PermissionPolicy,
|
||||
compileFsRule,
|
||||
compileFetchRule,
|
||||
globToRegex,
|
||||
} from "@suaveplan/api-sandbox-permissions/policy";
|
||||
|
||||
// Inspect compiled rules — used by adapter test fixtures to assert
|
||||
// the policy was built the way the manifest declared it.
|
||||
const policy = new PermissionPolicy({
|
||||
permissions: {
|
||||
fs: ["rw:./uploads/**", "./readonly/*.txt"],
|
||||
fetch: ["*.stripe.com", "http:localhost"],
|
||||
budgets: { cpuPercent: 25, memoryMb: 128, wallClockMs: 30_000 },
|
||||
},
|
||||
database: { required: true, schemaVersion: 1, tablePrefix: "billing_" },
|
||||
events: { publishes: ["billing.invoice.created"], subscribes: [] },
|
||||
secrets: [{ name: "STRIPE_SECRET_KEY", required: true }],
|
||||
});
|
||||
|
||||
const writeRule = policy.fs.find((rule) => rule.write);
|
||||
console.assert(writeRule?.root === "./uploads");
|
||||
|
||||
// Reusable: build a one-off rule outside a policy (for tests).
|
||||
const oneRule = compileFsRule("./tmp/*.log");
|
||||
console.assert(oneRule.test("./tmp/app.log"));
|
||||
|
||||
const httpsRule = compileFetchRule("api.stripe.com");
|
||||
console.assert(httpsRule.test("api.stripe.com"));
|
||||
|
||||
const re = globToRegex("./uploads/**");
|
||||
console.assert(re.test("./uploads/a/b/c"));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class PermissionPolicy`
|
||||
- `constructor(manifest: PolicyManifestView)` — throws
|
||||
`SandboxPermissionError(SAND_PERM.DENIED)` if the input fails the
|
||||
Zod schema.
|
||||
- `readonly fs: readonly CompiledFsRule[]`
|
||||
- `readonly fetch: readonly CompiledFetchRule[]`
|
||||
- `readonly budgets: ResolvedBudgets` (unset budgets default to `Infinity`).
|
||||
- `readonly manifest: PolicyManifestView`
|
||||
- `POLICY_MANIFEST_SCHEMA: ZodObject<…>` — manifest validation schema.
|
||||
- `compileFsRule(rule: string): CompiledFsRule`
|
||||
- `compileFetchRule(rule: string): CompiledFetchRule`
|
||||
- `normalisePath(input: string): string`
|
||||
- `globToRegex(pattern: string): RegExp`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The glob compiler is deliberately minimal — `*`, `**`, `?`, `[abc]` and
|
||||
nothing more. Sandbox permission lists are short and operator-authored,
|
||||
so the rules are written by hand against a constrained vocabulary.
|
||||
A full POSIX glob engine would add a dependency and a corresponding
|
||||
attack surface (catastrophic backtracking, character-class escapes)
|
||||
without buying any expressive power the manifest needs.
|
||||
|
||||
Path normalisation uses Node's `posix.normalize` so the same canonical
|
||||
form runs on Windows hosts. `..` segments are resolved before matching;
|
||||
the FS checker uses the rule's `root` field to reject paths that
|
||||
escape the allowed prefix even when the glob pattern alone would
|
||||
accept them.
|
||||
|
||||
The fetch host comparator lowercases both sides so case differences in
|
||||
the request URL never affect allow / deny.
|
||||
|
||||
Budgets default to `Number.POSITIVE_INFINITY` when unset — a manifest
|
||||
that omits a dimension grants unlimited usage for that dimension. The
|
||||
budget tracker treats `Infinity` as "never breach".
|
||||
@@ -0,0 +1,181 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
SAND_PERM_CODES,
|
||||
SandboxPermissionError,
|
||||
} from "../error-codes/error-codes.js";
|
||||
import {
|
||||
compileFetchRule,
|
||||
compileFsRule,
|
||||
globToRegex,
|
||||
normalisePath,
|
||||
PermissionPolicy,
|
||||
} from "./policy.js";
|
||||
|
||||
describe("PermissionPolicy", () => {
|
||||
it("PermissionPolicy reflects manifest verbatim", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const policy = new PermissionPolicy({
|
||||
permissions: {
|
||||
fs: ["./uploads/*"],
|
||||
fetch: ["api.stripe.com"],
|
||||
budgets: { wallClockMs: 30_000 },
|
||||
},
|
||||
});
|
||||
expect(policy.fs).toHaveLength(1);
|
||||
expect(policy.fs[0]?.source).toBe("./uploads/*");
|
||||
expect(policy.fs[0]?.write).toBe(false);
|
||||
expect(policy.fetch).toHaveLength(1);
|
||||
expect(policy.fetch[0]?.source).toBe("api.stripe.com");
|
||||
expect(policy.budgets.wallClockMs).toBe(30_000);
|
||||
expect(policy.budgets.cpuPercent).toBe(Number.POSITIVE_INFINITY);
|
||||
expect(policy.budgets.memoryMb).toBe(Number.POSITIVE_INFINITY);
|
||||
});
|
||||
|
||||
it("empty manifest yields empty arrays and Infinity budgets", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const policy = new PermissionPolicy({});
|
||||
expect(policy.fs).toEqual([]);
|
||||
expect(policy.fetch).toEqual([]);
|
||||
expect(policy.budgets.cpuPercent).toBe(Number.POSITIVE_INFINITY);
|
||||
expect(policy.budgets.memoryMb).toBe(Number.POSITIVE_INFINITY);
|
||||
expect(policy.budgets.wallClockMs).toBe(Number.POSITIVE_INFINITY);
|
||||
});
|
||||
|
||||
it("rw: prefix on fs rule sets write=true", { timeout: 5000 }, () => {
|
||||
const policy = new PermissionPolicy({
|
||||
permissions: { fs: ["rw:./uploads/*"] },
|
||||
});
|
||||
expect(policy.fs[0]?.write).toBe(true);
|
||||
});
|
||||
|
||||
it("invalid manifest throws SAND_PERM.DENIED", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
let err: SandboxPermissionError | undefined;
|
||||
try {
|
||||
// @ts-expect-error — intentionally invalid input shape
|
||||
new PermissionPolicy({ permissions: { fs: [123] } });
|
||||
} catch (e) {
|
||||
err = e as SandboxPermissionError;
|
||||
}
|
||||
expect(err).toBeInstanceOf(SandboxPermissionError);
|
||||
expect(err?.code).toBe(SAND_PERM_CODES.DENIED);
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalisePath", () => {
|
||||
it("resolves .. and strips trailing slashes", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(normalisePath("./uploads/../etc/passwd")).toBe("./etc/passwd");
|
||||
expect(normalisePath("./uploads/")).toBe("./uploads");
|
||||
expect(normalisePath("./uploads/inner/")).toBe("./uploads/inner");
|
||||
});
|
||||
|
||||
it("preserves absolute paths", { timeout: 5000 }, () => {
|
||||
expect(normalisePath("/var/log/")).toBe("/var/log");
|
||||
expect(normalisePath("/")).toBe("/");
|
||||
});
|
||||
|
||||
it("normalises backslashes to posix", { timeout: 5000 }, () => {
|
||||
expect(normalisePath(".\\uploads\\a")).toBe("./uploads/a");
|
||||
});
|
||||
|
||||
it("collapses current-dir input to .", { timeout: 5000 }, () => {
|
||||
expect(normalisePath(".")).toBe(".");
|
||||
});
|
||||
|
||||
it("keeps .. escape paths without re-prefixing", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(normalisePath("..")).toBe("..");
|
||||
expect(normalisePath("../sibling")).toBe("../sibling");
|
||||
});
|
||||
});
|
||||
|
||||
describe("globToRegex", () => {
|
||||
it("translates * to single-segment match", { timeout: 5000 }, () => {
|
||||
const re = globToRegex("./uploads/*");
|
||||
expect(re.test("./uploads/a.png")).toBe(true);
|
||||
expect(re.test("./uploads/sub/a.png")).toBe(false);
|
||||
});
|
||||
|
||||
it("translates ** to multi-segment match", { timeout: 5000 }, () => {
|
||||
const re = globToRegex("./uploads/**");
|
||||
expect(re.test("./uploads/a")).toBe(true);
|
||||
expect(re.test("./uploads/a/b/c")).toBe(true);
|
||||
});
|
||||
|
||||
it("translates ? to single non-slash character", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const re = globToRegex("a?c");
|
||||
expect(re.test("abc")).toBe(true);
|
||||
expect(re.test("a/c")).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves character class [abc]", { timeout: 5000 }, () => {
|
||||
const re = globToRegex("file.[tj]s");
|
||||
expect(re.test("file.ts")).toBe(true);
|
||||
expect(re.test("file.js")).toBe(true);
|
||||
expect(re.test("file.cs")).toBe(false);
|
||||
});
|
||||
|
||||
it("escapes regex metacharacters", { timeout: 5000 }, () => {
|
||||
const re = globToRegex("a.b+c(d)");
|
||||
expect(re.test("a.b+c(d)")).toBe(true);
|
||||
expect(re.test("aXbXc-d-")).toBe(false);
|
||||
});
|
||||
|
||||
it("treats unclosed [ as literal", { timeout: 5000 }, () => {
|
||||
const re = globToRegex("a[b");
|
||||
expect(re.test("a[b")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("compileFsRule + compileFetchRule", () => {
|
||||
it("compileFsRule extracts root prefix", { timeout: 5000 }, () => {
|
||||
const rule = compileFsRule("./uploads/*");
|
||||
expect(rule.root).toBe("./uploads");
|
||||
});
|
||||
|
||||
it("compileFsRule literal-only rule has whole path as root", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const rule = compileFsRule("./uploads/exact.txt");
|
||||
expect(rule.root).toBe("./uploads/exact.txt");
|
||||
expect(rule.test("./uploads/exact.txt")).toBe(true);
|
||||
});
|
||||
|
||||
it("compileFsRule wildcard-only with no slash before star", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const rule = compileFsRule("*.txt");
|
||||
// root falls back to the empty prefix when no '/' precedes the wildcard
|
||||
expect(rule.root).toBe("");
|
||||
expect(rule.test("a.txt")).toBe(true);
|
||||
});
|
||||
|
||||
it("compileFetchRule recognises *.suffix", { timeout: 5000 }, () => {
|
||||
const rule = compileFetchRule("*.stripe.com");
|
||||
expect(rule.test("eu.api.stripe.com")).toBe(true);
|
||||
expect(rule.test("stripe.com")).toBe(false);
|
||||
});
|
||||
|
||||
it("compileFetchRule recognises http: prefix", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const rule = compileFetchRule("http:localhost");
|
||||
expect(rule.allowHttp).toBe(true);
|
||||
expect(rule.test("localhost")).toBe(true);
|
||||
});
|
||||
|
||||
it("compileFetchRule exact match is case-insensitive", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const rule = compileFetchRule("API.Stripe.COM");
|
||||
expect(rule.test("api.stripe.com")).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,254 @@
|
||||
import { posix } from "node:path";
|
||||
import z from "zod";
|
||||
import {
|
||||
SAND_PERM_CODES,
|
||||
SandboxPermissionError,
|
||||
} from "../error-codes/error-codes.js";
|
||||
import type {
|
||||
CompiledFetchRule,
|
||||
CompiledFsRule,
|
||||
PolicyManifestView,
|
||||
ResolvedBudgets,
|
||||
} from "./policy.types.js";
|
||||
|
||||
/**
|
||||
* Zod schema applied at the ingestion boundary. The runtime types in
|
||||
* `@suaveplan/api-types` already model the same shape structurally;
|
||||
* this schema gives us the trust boundary for untrusted manifest
|
||||
* payloads (worker boundary, hot-reload, admin patch).
|
||||
*/
|
||||
export const POLICY_MANIFEST_SCHEMA = z
|
||||
.object({
|
||||
permissions: z
|
||||
.object({
|
||||
fs: z.array(z.string().min(1)).optional(),
|
||||
fetch: z.array(z.string().min(1)).optional(),
|
||||
budgets: z
|
||||
.object({
|
||||
cpuPercent: z.number().positive().optional(),
|
||||
memoryMb: z.number().positive().optional(),
|
||||
wallClockMs: z.number().positive().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
events: z
|
||||
.object({
|
||||
publishes: z.array(z.string().min(1)).optional(),
|
||||
subscribes: z.array(z.string().min(1)).optional(),
|
||||
})
|
||||
.optional(),
|
||||
database: z
|
||||
.object({
|
||||
required: z.boolean(),
|
||||
schemaVersion: z.number(),
|
||||
tablePrefix: z.string().min(1).optional(),
|
||||
tenantIsolation: z
|
||||
.enum(["row-level", "schema", "database"])
|
||||
.optional(),
|
||||
preferredEngine: z.string().optional(),
|
||||
compatibleEngines: z.array(z.string()).optional(),
|
||||
migrationsDir: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
secrets: z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string().min(1),
|
||||
required: z.boolean(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
/**
|
||||
* Normalise a filesystem path for comparison against the allowlist.
|
||||
* Resolves `..` segments and strips trailing slashes. Always returns a
|
||||
* POSIX-style path; rules and check inputs are compared on the same
|
||||
* canonical form regardless of host platform.
|
||||
*/
|
||||
export function normalisePath(input: string): string {
|
||||
const cleaned = input.replace(/\\/g, "/");
|
||||
if (cleaned.startsWith("/")) {
|
||||
return posix.normalize(cleaned).replace(/\/+$/, "") || "/";
|
||||
}
|
||||
const hadDot = cleaned.startsWith("./") || cleaned === ".";
|
||||
const normalised = posix.normalize(cleaned).replace(/\/+$/, "");
|
||||
if (normalised === ".") {
|
||||
return ".";
|
||||
}
|
||||
if (normalised.startsWith("../") || normalised === "..") {
|
||||
// Path escaped its starting point — keep as-is (no leading ./).
|
||||
return normalised;
|
||||
}
|
||||
if (hadDot) {
|
||||
return `./${normalised}`;
|
||||
}
|
||||
return normalised;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile a single `permissions.fs[]` entry into a {@link CompiledFsRule}.
|
||||
* The optional `rw:` prefix toggles write access; everything else is
|
||||
* read-only.
|
||||
*/
|
||||
export function compileFsRule(rule: string): CompiledFsRule {
|
||||
let source = rule;
|
||||
let write = false;
|
||||
if (rule.startsWith("rw:")) {
|
||||
write = true;
|
||||
source = rule.slice(3);
|
||||
}
|
||||
const normalised = normalisePath(source);
|
||||
const root = extractGlobRoot(normalised);
|
||||
const pattern = globToRegex(normalised);
|
||||
return {
|
||||
source: rule,
|
||||
write,
|
||||
root,
|
||||
test(resolved: string): boolean {
|
||||
return pattern.test(resolved);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the literal-path prefix from a glob pattern (everything up to
|
||||
* the first `*` / `?` / `[`). Used as the deny-reason boundary check —
|
||||
* resolved paths that escape this prefix are rejected before the regex
|
||||
* even runs.
|
||||
*/
|
||||
function extractGlobRoot(pattern: string): string {
|
||||
const wildcardAt = pattern.search(/[*?[]/);
|
||||
if (wildcardAt === -1) {
|
||||
return pattern;
|
||||
}
|
||||
const prefix = pattern.slice(0, wildcardAt);
|
||||
const lastSlash = prefix.lastIndexOf("/");
|
||||
return lastSlash >= 0 ? prefix.slice(0, lastSlash) : prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a simple glob pattern (`*`, `**`, `?`, `[abc]`) into a
|
||||
* Regex with anchored ends. The transformation is intentionally minimal
|
||||
* — sandboxed permissions are simple allowlists, not full POSIX glob.
|
||||
*/
|
||||
export function globToRegex(pattern: string): RegExp {
|
||||
let out = "";
|
||||
let i = 0;
|
||||
while (i < pattern.length) {
|
||||
const ch = pattern[i] as string;
|
||||
if (ch === "*") {
|
||||
const next = pattern[i + 1];
|
||||
if (next === "*") {
|
||||
out += ".*";
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
out += "[^/]*";
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === "?") {
|
||||
out += "[^/]";
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === "[") {
|
||||
const close = pattern.indexOf("]", i + 1);
|
||||
if (close !== -1) {
|
||||
out += pattern.slice(i, close + 1);
|
||||
i = close + 1;
|
||||
continue;
|
||||
}
|
||||
// Unclosed bracket — treat as literal.
|
||||
out += "\\[";
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (/[.+^${}()|\\]/.test(ch)) {
|
||||
out += `\\${ch}`;
|
||||
} else {
|
||||
out += ch;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
return new RegExp(`^${out}$`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile a single `permissions.fetch[]` host pattern. Optional
|
||||
* `http:` prefix toggles insecure-scheme permission.
|
||||
*/
|
||||
export function compileFetchRule(rule: string): CompiledFetchRule {
|
||||
let allowHttp = false;
|
||||
let source = rule;
|
||||
if (rule.startsWith("http:")) {
|
||||
allowHttp = true;
|
||||
source = rule.slice(5);
|
||||
}
|
||||
const lowered = source.toLowerCase();
|
||||
if (lowered.startsWith("*.")) {
|
||||
const suffix = lowered.slice(2);
|
||||
return {
|
||||
source: rule,
|
||||
allowHttp,
|
||||
test(host) {
|
||||
return host.endsWith(`.${suffix}`);
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
source: rule,
|
||||
allowHttp,
|
||||
test(host) {
|
||||
return host === lowered;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolved permission policy. Built once per plugin start; consumed
|
||||
* by `PermissionChecker` and `BudgetTracker`.
|
||||
*/
|
||||
export class PermissionPolicy {
|
||||
public readonly fs: readonly CompiledFsRule[];
|
||||
public readonly fetch: readonly CompiledFetchRule[];
|
||||
public readonly budgets: ResolvedBudgets;
|
||||
public readonly manifest: PolicyManifestView;
|
||||
|
||||
/**
|
||||
* Construct from a manifest view. The manifest is validated via
|
||||
* {@link POLICY_MANIFEST_SCHEMA} so untrusted ingestion paths
|
||||
* cannot bypass the contract. Throws `SandboxPermissionError`
|
||||
* (code `SAND_PERM.DENIED`) when the input does not satisfy the
|
||||
* schema — same code surface as a deny at request time, so
|
||||
* upstream catch-blocks need only one branch.
|
||||
*/
|
||||
public constructor(manifest: PolicyManifestView) {
|
||||
const parsed = POLICY_MANIFEST_SCHEMA.safeParse(manifest);
|
||||
if (!parsed.success) {
|
||||
throw new SandboxPermissionError(
|
||||
SAND_PERM_CODES.DENIED,
|
||||
"sandbox permission manifest failed validation",
|
||||
{
|
||||
context: {
|
||||
capability: "policy",
|
||||
reason: "manifest-invalid",
|
||||
},
|
||||
cause: parsed.error,
|
||||
}
|
||||
);
|
||||
}
|
||||
this.manifest = manifest;
|
||||
this.fs = (manifest.permissions?.fs ?? []).map(compileFsRule);
|
||||
this.fetch = (manifest.permissions?.fetch ?? []).map(compileFetchRule);
|
||||
const budgets = manifest.permissions?.budgets ?? {};
|
||||
this.budgets = {
|
||||
cpuPercent: budgets.cpuPercent ?? Number.POSITIVE_INFINITY,
|
||||
memoryMb: budgets.memoryMb ?? Number.POSITIVE_INFINITY,
|
||||
wallClockMs: budgets.wallClockMs ?? Number.POSITIVE_INFINITY,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import type {
|
||||
DatabaseManifest,
|
||||
EventManifest,
|
||||
PermissionsManifest,
|
||||
} from "@suaveplan/api-types/manifest";
|
||||
|
||||
/** Compiled glob row used by the filesystem checker. */
|
||||
export interface CompiledFsRule {
|
||||
/** Original rule string (kept for diagnostics — never leaked into errors). */
|
||||
readonly source: string;
|
||||
/** `true` when the rule was prefixed `rw:` — write access is permitted. */
|
||||
readonly write: boolean;
|
||||
/** Normalised allowlist root (path-prefix). */
|
||||
readonly root: string;
|
||||
/** Compiled glob test for the suffix portion (after the root). */
|
||||
test(resolved: string): boolean;
|
||||
}
|
||||
|
||||
/** Compiled host row used by the fetch checker. */
|
||||
export interface CompiledFetchRule {
|
||||
readonly source: string;
|
||||
/** When `true`, the rule permits insecure `http:` requests. */
|
||||
readonly allowHttp: boolean;
|
||||
/** Test the URL's host (lowercased, no port). */
|
||||
test(host: string): boolean;
|
||||
}
|
||||
|
||||
/** Resolved per-request budgets, with `Infinity` for unset dimensions. */
|
||||
export interface ResolvedBudgets {
|
||||
readonly cpuPercent: number;
|
||||
readonly memoryMb: number;
|
||||
readonly wallClockMs: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The portion of the {@link PluginManifest} this layer needs at
|
||||
* construction time. Kept narrow so the layer does not depend on
|
||||
* unrelated fields (routes, jobs, telemetry, etc.).
|
||||
*/
|
||||
export interface PolicyManifestView {
|
||||
/** Permissions block per design doc §3. */
|
||||
readonly permissions?: PermissionsManifest;
|
||||
/** Declared event topics — used by capability gating. */
|
||||
readonly events?: EventManifest;
|
||||
/** Declared database surface — used by capability gating. */
|
||||
readonly database?: DatabaseManifest;
|
||||
/** Manifest-declared secret entries. */
|
||||
readonly secrets?: ReadonlyArray<{
|
||||
readonly name: string;
|
||||
readonly required: boolean;
|
||||
}>;
|
||||
}
|
||||
Reference in New Issue
Block a user