Track HiveGrid Cloud planning reference
This commit is contained in:
1 parent
b3d5ea6764
commit
e341c5938a
5784 files changed
+1104929
-3
No files matched your search
@@ -0,0 +1,332 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { CapabilityDeniedError, createCapabilityGateway } from "./capability-gateway.js";
|
||||
import type { PluginManifest } from "./manifest.js";
|
||||
|
||||
const DEFERRED_HOST_BINDINGS = [
|
||||
"http.outbound",
|
||||
"secret.read",
|
||||
"db.read",
|
||||
"db.write",
|
||||
"storage.read",
|
||||
"storage.write",
|
||||
"tenant.info",
|
||||
] as const;
|
||||
|
||||
function fixture(capabilities: PluginManifest["capabilities"] = []): PluginManifest {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
id: "de.toolctrl.test",
|
||||
version: "1.0.0",
|
||||
name: "Test",
|
||||
entry: "plugin.wasm",
|
||||
wasmSha256: "0".repeat(64),
|
||||
signature: "0".repeat(128),
|
||||
signedBy: "0".repeat(64),
|
||||
allowedHosts: [],
|
||||
capabilities,
|
||||
hooks: [{ name: "x", export: "x" }],
|
||||
};
|
||||
}
|
||||
|
||||
describe("createCapabilityGateway", () => {
|
||||
test("invokes a host-fn whose capability is declared", async () => {
|
||||
const gateway = createCapabilityGateway({
|
||||
manifest: fixture([{ name: "log" }]),
|
||||
hostFns: {
|
||||
log: (msg: unknown) => `logged:${String(msg)}`,
|
||||
},
|
||||
});
|
||||
const result = await gateway.invoke<string>("log", "hi");
|
||||
expect(result).toBe("logged:hi");
|
||||
});
|
||||
|
||||
test("denies undeclared capabilities with not_declared_in_manifest", async () => {
|
||||
const gateway = createCapabilityGateway({
|
||||
manifest: fixture([{ name: "log" }]),
|
||||
hostFns: {
|
||||
"http.outbound": async () => "should not run",
|
||||
},
|
||||
});
|
||||
await expect(gateway.invoke("http.outbound")).rejects.toThrow(CapabilityDeniedError);
|
||||
await expect(gateway.invoke("http.outbound")).rejects.toMatchObject({
|
||||
reason: "not_declared_in_manifest",
|
||||
});
|
||||
});
|
||||
|
||||
test("denies declared-but-unimplemented capabilities", async () => {
|
||||
const gateway = createCapabilityGateway({
|
||||
manifest: fixture([{ name: "secret.read" }]),
|
||||
hostFns: {},
|
||||
});
|
||||
await expect(gateway.invoke("secret.read")).rejects.toMatchObject({
|
||||
reason: "no_host_implementation",
|
||||
});
|
||||
});
|
||||
|
||||
test.each(DEFERRED_HOST_BINDINGS)("fails closed for deferred %s bindings with no_host_implementation", async (capability) => {
|
||||
const hostFn = vi.fn(async () => "should not run");
|
||||
const gateway = createCapabilityGateway({
|
||||
manifest: fixture([{ name: capability }]),
|
||||
approvedGrants: [{ capabilityName: capability, scopeJson: null }],
|
||||
hostFns: {
|
||||
log: () => "ok",
|
||||
audit: () => "ok",
|
||||
[capability]: hostFn,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(gateway.invoke(capability)).rejects.toMatchObject({
|
||||
reason: "no_host_implementation",
|
||||
});
|
||||
expect(hostFn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("denies manifest-declared capabilities without tenant-approved grants", async () => {
|
||||
const gateway = createCapabilityGateway({
|
||||
manifest: fixture([{ name: "log" }, { name: "audit" }]),
|
||||
approvedGrants: [{ capabilityName: "log", scopeJson: null }],
|
||||
hostFns: {
|
||||
log: () => "ok",
|
||||
audit: () => "should not run",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(gateway.invoke("log")).resolves.toBe("ok");
|
||||
await expect(gateway.invoke("audit")).rejects.toMatchObject({
|
||||
reason: "not_granted_for_tenant",
|
||||
});
|
||||
});
|
||||
|
||||
test("denies tenant grants whose scope exceeds the manifest scope", async () => {
|
||||
const hostFn = vi.fn(async () => "should not run");
|
||||
const gateway = createCapabilityGateway({
|
||||
manifest: fixture([{ name: "http.outbound", scope: { allowedHosts: ["api.example.com"] } }]),
|
||||
approvedGrants: [{ capabilityName: "http.outbound", scopeJson: { allowedHosts: ["api.example.com", "internal.example"] } }],
|
||||
hostFns: {
|
||||
"http.outbound": hostFn,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(gateway.invoke("http.outbound", "https://api.example.com")).rejects.toMatchObject({
|
||||
reason: "not_granted_for_tenant",
|
||||
});
|
||||
expect(hostFn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("charges fuel per call (defaults to 100)", async () => {
|
||||
const gateway = createCapabilityGateway({
|
||||
manifest: fixture([{ name: "log" }]),
|
||||
hostFns: { log: () => "ok" },
|
||||
});
|
||||
expect(gateway.fuelCharged()).toBe(0);
|
||||
await gateway.invoke("log");
|
||||
expect(gateway.fuelCharged()).toBe(100);
|
||||
await gateway.invoke("log");
|
||||
expect(gateway.fuelCharged()).toBe(200);
|
||||
});
|
||||
|
||||
test("respects fuelCostPerCall override and resetFuel", async () => {
|
||||
const gateway = createCapabilityGateway({
|
||||
manifest: fixture([{ name: "log" }]),
|
||||
hostFns: { log: () => "ok" },
|
||||
fuelCostPerCall: 25,
|
||||
});
|
||||
await gateway.invoke("log");
|
||||
await gateway.invoke("log");
|
||||
expect(gateway.fuelCharged()).toBe(50);
|
||||
gateway.resetFuel();
|
||||
expect(gateway.fuelCharged()).toBe(0);
|
||||
});
|
||||
|
||||
test("calls onDenied with the deny reason for both deny paths", async () => {
|
||||
const denials: Array<{ capability: string; reason: string }> = [];
|
||||
const gateway = createCapabilityGateway({
|
||||
manifest: fixture([{ name: "secret.read" }]),
|
||||
hostFns: {},
|
||||
onDenied: (info) => denials.push(info),
|
||||
});
|
||||
await expect(gateway.invoke("http.outbound")).rejects.toThrow();
|
||||
await expect(gateway.invoke("secret.read")).rejects.toThrow();
|
||||
expect(denials.map((d) => d.reason)).toEqual([
|
||||
"not_declared_in_manifest",
|
||||
"no_host_implementation",
|
||||
]);
|
||||
});
|
||||
|
||||
test("records onCall outcomes for success, denial, and host-fn errors", async () => {
|
||||
const calls: Array<{ capability: string; outcome: string; errorCode?: string }> = [];
|
||||
const gateway = createCapabilityGateway({
|
||||
manifest: fixture([{ name: "log" }, { name: "audit" }]),
|
||||
hostFns: {
|
||||
log: () => "ok",
|
||||
audit: () => {
|
||||
throw new TypeError("audit sink unavailable");
|
||||
},
|
||||
},
|
||||
onCall: (info) => calls.push(info),
|
||||
});
|
||||
|
||||
await expect(gateway.invoke("log")).resolves.toBe("ok");
|
||||
await expect(gateway.invoke("http.outbound")).rejects.toMatchObject({
|
||||
reason: "not_declared_in_manifest",
|
||||
});
|
||||
await expect(gateway.invoke("db.read")).rejects.toMatchObject({
|
||||
reason: "not_declared_in_manifest",
|
||||
});
|
||||
await expect(gateway.invoke("audit")).rejects.toThrow("audit sink unavailable");
|
||||
|
||||
expect(calls).toMatchObject([
|
||||
{ capability: "log", outcome: "success" },
|
||||
{ capability: "http.outbound", outcome: "denied", errorCode: "not_declared_in_manifest" },
|
||||
{ capability: "db.read", outcome: "denied", errorCode: "not_declared_in_manifest" },
|
||||
{ capability: "audit", outcome: "error", errorCode: "TypeError" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("records deterministic payload hashes without exposing raw args", async () => {
|
||||
const calls: Array<Record<string, unknown>> = [];
|
||||
const gateway = createCapabilityGateway({
|
||||
manifest: fixture([{ name: "log" }]),
|
||||
hostFns: {
|
||||
log: () => "ok",
|
||||
},
|
||||
onCall: (info) => calls.push(info),
|
||||
});
|
||||
|
||||
await gateway.invoke("log", "same-value");
|
||||
await gateway.invoke("log", "same-value");
|
||||
await gateway.invoke("log", "other-value");
|
||||
|
||||
expect(calls[0]?.payloadHash).toMatch(/^[a-f0-9]{64}$/u);
|
||||
expect(calls[1]?.payloadHash).toBe(calls[0]?.payloadHash);
|
||||
expect(calls[2]?.payloadHash).not.toBe(calls[0]?.payloadHash);
|
||||
expect(JSON.stringify(calls)).not.toContain("same-value");
|
||||
expect(JSON.stringify(calls)).not.toContain("other-value");
|
||||
});
|
||||
|
||||
test("records no_host_implementation through onCall", async () => {
|
||||
const calls: Array<{ capability: string; outcome: string; errorCode?: string }> = [];
|
||||
const gateway = createCapabilityGateway({
|
||||
manifest: fixture([{ name: "secret.read" }]),
|
||||
hostFns: {},
|
||||
onCall: (info) => calls.push(info),
|
||||
});
|
||||
|
||||
await expect(gateway.invoke("secret.read")).rejects.toMatchObject({
|
||||
reason: "no_host_implementation",
|
||||
});
|
||||
|
||||
expect(calls).toEqual([
|
||||
expect.objectContaining({
|
||||
capability: "secret.read",
|
||||
outcome: "denied",
|
||||
errorCode: "no_host_implementation",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test("keeps future scoped host bindings unavailable before dispatch", async () => {
|
||||
const hostFn = vi.fn(async () => "should not run");
|
||||
const denials: Array<{ capability: string; reason: string }> = [];
|
||||
const calls: Array<{ capability: string; outcome: string; errorCode?: string }> = [];
|
||||
const gateway = createCapabilityGateway({
|
||||
manifest: fixture([{ name: "http.outbound", scope: { allowedHosts: ["api.example.com"] } }]),
|
||||
approvedGrants: [{ capabilityName: "http.outbound", scopeJson: { allowedHosts: ["api.example.com"] } }],
|
||||
hostFns: {
|
||||
"http.outbound": hostFn,
|
||||
},
|
||||
onDenied: (info) => denials.push(info),
|
||||
onCall: (info) => calls.push(info),
|
||||
});
|
||||
|
||||
await expect(gateway.invoke("http.outbound", { url: "https://evil.example.com/v1" })).rejects.toMatchObject({
|
||||
reason: "no_host_implementation",
|
||||
});
|
||||
expect(hostFn).not.toHaveBeenCalled();
|
||||
expect(denials).toEqual([{ capability: "http.outbound", reason: "no_host_implementation" }]);
|
||||
expect(calls).toEqual([
|
||||
expect.objectContaining({
|
||||
capability: "http.outbound",
|
||||
outcome: "denied",
|
||||
errorCode: "no_host_implementation",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test("ignores onCall recorder failures", async () => {
|
||||
const gateway = createCapabilityGateway({
|
||||
manifest: fixture([{ name: "log" }]),
|
||||
hostFns: { log: () => "ok" },
|
||||
onCall: () => {
|
||||
throw new Error("audit sink down");
|
||||
},
|
||||
});
|
||||
|
||||
await expect(gateway.invoke("log")).resolves.toBe("ok");
|
||||
});
|
||||
|
||||
test("propagates async reviewed host-fn results unchanged", async () => {
|
||||
const gateway = createCapabilityGateway({
|
||||
manifest: fixture([{ name: "audit" }]),
|
||||
hostFns: {
|
||||
audit: async () => ({ recorded: true }),
|
||||
},
|
||||
});
|
||||
const out = await gateway.invoke<{ recorded: boolean }>("audit");
|
||||
expect(out).toEqual({ recorded: true });
|
||||
});
|
||||
|
||||
test.each(DEFERRED_HOST_BINDINGS)("invokeSync fails closed for deferred %s bindings even when wired", (capability) => {
|
||||
const hostFn = vi.fn(() => "should not run");
|
||||
const gateway = createCapabilityGateway({
|
||||
manifest: fixture([{ name: capability }]),
|
||||
approvedGrants: [{ capabilityName: capability, scopeJson: null }],
|
||||
hostFns: {
|
||||
[capability]: hostFn,
|
||||
},
|
||||
});
|
||||
|
||||
expect(() => gateway.invokeSync(capability)).toThrow(CapabilityDeniedError);
|
||||
expect(() => gateway.invokeSync(capability)).toThrow(`capability denied: ${capability} (no_host_implementation)`);
|
||||
expect(hostFn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("invokeSync gates synchronous host imports and records calls", () => {
|
||||
const calls: Array<{ capability: string; outcome: string; errorCode?: string }> = [];
|
||||
const gateway = createCapabilityGateway({
|
||||
manifest: fixture([{ name: "log" }]),
|
||||
hostFns: {
|
||||
log: (msg: unknown) => `logged:${String(msg)}`,
|
||||
},
|
||||
onCall: (info) => calls.push(info),
|
||||
});
|
||||
|
||||
expect(gateway.invokeSync("log", "hi")).toBe("logged:hi");
|
||||
expect(gateway.fuelCharged()).toBe(100);
|
||||
expect(() => gateway.invokeSync("audit", { action: "x" })).toThrow(CapabilityDeniedError);
|
||||
expect(calls).toMatchObject([
|
||||
{ capability: "log", outcome: "success" },
|
||||
{ capability: "audit", outcome: "denied", errorCode: "not_declared_in_manifest" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("invokeSync rejects async host functions before reporting success", () => {
|
||||
const calls: Array<{ capability: string; outcome: string; errorCode?: string }> = [];
|
||||
const gateway = createCapabilityGateway({
|
||||
manifest: fixture([{ name: "audit" }]),
|
||||
hostFns: {
|
||||
audit: async () => "later",
|
||||
},
|
||||
onCall: (info) => calls.push(info),
|
||||
});
|
||||
|
||||
expect(() => gateway.invokeSync("audit")).toThrow("async_host_fn_not_supported");
|
||||
expect(calls).toEqual([
|
||||
expect.objectContaining({
|
||||
capability: "audit",
|
||||
outcome: "error",
|
||||
errorCode: "Error",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user