@fusionkit/plane 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/auth.d.ts +18 -0
  2. package/dist/auth.js +46 -0
  3. package/dist/claim-token-service.d.ts +23 -0
  4. package/dist/claim-token-service.js +54 -0
  5. package/dist/contract-service.d.ts +14 -0
  6. package/dist/contract-service.js +39 -0
  7. package/dist/domain-errors.d.ts +13 -0
  8. package/dist/domain-errors.js +31 -0
  9. package/dist/idp.d.ts +26 -0
  10. package/dist/idp.js +24 -0
  11. package/dist/index.d.ts +35 -0
  12. package/dist/index.js +21 -0
  13. package/dist/keys.d.ts +60 -0
  14. package/dist/keys.js +132 -0
  15. package/dist/logging.d.ts +21 -0
  16. package/dist/logging.js +42 -0
  17. package/dist/plane.d.ts +167 -0
  18. package/dist/plane.js +606 -0
  19. package/dist/policy.d.ts +23 -0
  20. package/dist/policy.js +92 -0
  21. package/dist/ratelimit.d.ts +40 -0
  22. package/dist/ratelimit.js +94 -0
  23. package/dist/receipt-service.d.ts +16 -0
  24. package/dist/receipt-service.js +17 -0
  25. package/dist/retention.d.ts +33 -0
  26. package/dist/retention.js +123 -0
  27. package/dist/run-lifecycle.d.ts +2 -0
  28. package/dist/run-lifecycle.js +19 -0
  29. package/dist/secrets.d.ts +25 -0
  30. package/dist/secrets.js +73 -0
  31. package/dist/server.d.ts +38 -0
  32. package/dist/server.js +418 -0
  33. package/dist/sqlite-store.d.ts +53 -0
  34. package/dist/sqlite-store.js +401 -0
  35. package/dist/store.d.ts +107 -0
  36. package/dist/store.js +9 -0
  37. package/dist/test/api.test.d.ts +1 -0
  38. package/dist/test/api.test.js +179 -0
  39. package/dist/test/hardening.test.d.ts +1 -0
  40. package/dist/test/hardening.test.js +259 -0
  41. package/dist/test/policy.test.d.ts +1 -0
  42. package/dist/test/policy.test.js +78 -0
  43. package/dist/test/server-hardening.test.d.ts +1 -0
  44. package/dist/test/server-hardening.test.js +192 -0
  45. package/dist/test/ui-parity.test.d.ts +1 -0
  46. package/dist/test/ui-parity.test.js +28 -0
  47. package/dist/validation.d.ts +326 -0
  48. package/dist/validation.js +178 -0
  49. package/package.json +34 -0
  50. package/ui/app.css +276 -0
  51. package/ui/app.js +483 -0
  52. package/ui/index.html +65 -0
@@ -0,0 +1,179 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { after, before, test } from "node:test";
6
+ import { generateEd25519KeyPair } from "@fusionkit/protocol";
7
+ import { generateMasterKeyHex, masterKeyFromMaterial } from "../keys.js";
8
+ import { Plane } from "../plane.js";
9
+ import { defaultPolicy } from "../policy.js";
10
+ import { SecretStore } from "../secrets.js";
11
+ import { startPlaneServer } from "../server.js";
12
+ const ADMIN = "api-test-admin";
13
+ let dataDir;
14
+ let baseUrl;
15
+ let server;
16
+ function manifestFixture() {
17
+ return {
18
+ version: "warrant.manifest.v1",
19
+ baseRef: "0".repeat(40),
20
+ bundleHash: "1".repeat(64),
21
+ untrackedFiles: [],
22
+ deniedPatterns: [],
23
+ deniedPaths: []
24
+ };
25
+ }
26
+ function requestFixture(overrides = {}) {
27
+ return {
28
+ requestedBy: { kind: "human", id: "api-tester" },
29
+ agentKind: "mock",
30
+ prompt: "api test task",
31
+ pool: "default",
32
+ secretNames: [],
33
+ workspace: manifestFixture(),
34
+ network: { defaultDeny: true, allowHosts: [] },
35
+ budget: {},
36
+ disclosure: "minimal-context",
37
+ ...overrides
38
+ };
39
+ }
40
+ async function http(method, path, options = {}) {
41
+ const headers = {};
42
+ if (options.token)
43
+ headers.authorization = `Bearer ${options.token}`;
44
+ if (options.body !== undefined)
45
+ headers["content-type"] = "application/json";
46
+ const response = await fetch(`${baseUrl}${path}`, {
47
+ method,
48
+ headers,
49
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
50
+ redirect: "manual"
51
+ });
52
+ const contentType = response.headers.get("content-type") ?? "";
53
+ const body = contentType.includes("application/json")
54
+ ? await response.json()
55
+ : await response.text();
56
+ return { status: response.status, body, contentType };
57
+ }
58
+ before(async () => {
59
+ dataDir = mkdtempSync(join(tmpdir(), "warrant-api-test-"));
60
+ const keys = generateEd25519KeyPair();
61
+ const policy = defaultPolicy();
62
+ policy.consent = [{ when: "agent-kind", match: "codex", approvers: ["sec"] }];
63
+ policy.agents.allow = ["mock", "codex"];
64
+ const plane = new Plane({
65
+ dataDir: join(dataDir, "data"),
66
+ policy,
67
+ planePrivateKeyPem: keys.privateKeyPem,
68
+ planePublicKeyPem: keys.publicKeyPem,
69
+ adminToken: ADMIN,
70
+ enrollToken: "api-test-enroll",
71
+ secretStore: new SecretStore(join(dataDir, "secrets.enc"), masterKeyFromMaterial(generateMasterKeyHex()))
72
+ });
73
+ const started = await startPlaneServer(plane, { port: 0 });
74
+ server = started.server;
75
+ baseUrl = `http://127.0.0.1:${started.port}`;
76
+ });
77
+ after(() => {
78
+ server.close(() => undefined);
79
+ rmSync(dataDir, { recursive: true, force: true });
80
+ });
81
+ test("health endpoint requires no auth", async () => {
82
+ const { status, body } = await http("GET", "/v1/health");
83
+ assert.equal(status, 200);
84
+ assert.deepEqual(body, { ok: true, service: "warrant-plane" });
85
+ });
86
+ test("root redirects to the control panel, which serves all assets", async () => {
87
+ const root = await fetch(`${baseUrl}/`, { redirect: "manual" });
88
+ assert.equal(root.status, 302);
89
+ assert.equal(root.headers.get("location"), "/ui/");
90
+ await root.arrayBuffer();
91
+ const page = await http("GET", "/ui/");
92
+ assert.equal(page.status, 200);
93
+ assert.match(page.contentType, /text\/html/);
94
+ assert.match(String(page.body), /Warrant — Control Panel/);
95
+ const css = await http("GET", "/ui/app.css");
96
+ assert.equal(css.status, 200);
97
+ assert.match(css.contentType, /text\/css/);
98
+ const js = await http("GET", "/ui/app.js");
99
+ assert.equal(js.status, 200);
100
+ assert.match(js.contentType, /text\/javascript/);
101
+ assert.match(String(js.body), /control panel/);
102
+ });
103
+ test("admin endpoints fail closed without the admin token", async () => {
104
+ for (const path of ["/v1/runs", "/v1/runners", "/v1/policy", "/v1/export"]) {
105
+ const { status } = await http("GET", path);
106
+ assert.equal(status, 401, `${path} must require auth`);
107
+ }
108
+ const { status } = await http("GET", "/v1/runs", { token: "wrong" });
109
+ assert.equal(status, 401);
110
+ });
111
+ test("policy endpoint returns the snapshot and its hash", async () => {
112
+ const { status, body } = await http("GET", "/v1/policy", { token: ADMIN });
113
+ assert.equal(status, 200);
114
+ const snapshot = body;
115
+ assert.equal(snapshot.policy.version, "warrant.policy.v1");
116
+ assert.match(snapshot.policyHash, /^[0-9a-f]{64}$/);
117
+ });
118
+ test("runs can be listed, and unclaimed runs can be cancelled", async () => {
119
+ const created = await http("POST", "/v1/runs", {
120
+ token: ADMIN,
121
+ body: { request: requestFixture() }
122
+ });
123
+ assert.equal(created.status, 200);
124
+ const { runId } = created.body;
125
+ const list = await http("GET", "/v1/runs", { token: ADMIN });
126
+ assert.equal(list.status, 200);
127
+ const { runs } = list.body;
128
+ const row = runs.find((r) => r.runId === runId);
129
+ assert.ok(row, "created run must appear in the list");
130
+ assert.equal(row.status, "created");
131
+ assert.equal(row.agentKind, "mock");
132
+ assert.equal(row.hasReceipt, false);
133
+ const cancelled = await http("POST", `/v1/runs/${runId}/cancel`, {
134
+ token: ADMIN,
135
+ body: { actor: { kind: "human", id: "api-tester" } }
136
+ });
137
+ assert.equal(cancelled.status, 200);
138
+ assert.deepEqual(cancelled.body, { runId, status: "cancelled" });
139
+ const again = await http("POST", `/v1/runs/${runId}/cancel`, {
140
+ token: ADMIN,
141
+ body: { actor: { kind: "human", id: "api-tester" } }
142
+ });
143
+ assert.equal(again.status, 400, "terminal runs cannot be cancelled twice");
144
+ });
145
+ test("awaiting-approval runs can be cancelled before any contract exists", async () => {
146
+ const created = await http("POST", "/v1/runs", {
147
+ token: ADMIN,
148
+ body: { request: requestFixture({ agentKind: "codex", pool: "default" }) }
149
+ });
150
+ assert.equal(created.status, 200);
151
+ const { runId, status } = created.body;
152
+ assert.equal(status, "awaiting_approval");
153
+ const cancelled = await http("POST", `/v1/runs/${runId}/cancel`, {
154
+ token: ADMIN,
155
+ body: { actor: { kind: "human", id: "sec" } }
156
+ });
157
+ assert.equal(cancelled.status, 200);
158
+ assert.deepEqual(cancelled.body, { runId, status: "cancelled" });
159
+ });
160
+ test("runner listing reports enrolled runners without token material", async () => {
161
+ const enrolled = await http("POST", "/v1/runners/enroll", {
162
+ body: {
163
+ enrollToken: "api-test-enroll",
164
+ publicKeyPem: generateEd25519KeyPair().publicKeyPem,
165
+ pool: "default"
166
+ }
167
+ });
168
+ assert.equal(enrolled.status, 200);
169
+ const { status, body } = await http("GET", "/v1/runners", { token: ADMIN });
170
+ assert.equal(status, 200);
171
+ const { runners } = body;
172
+ assert.ok(runners.length >= 1);
173
+ const runner = runners[0];
174
+ assert.ok(runner);
175
+ assert.match(runner.runnerId ?? "", /^rnr_/);
176
+ assert.match(runner.keyId ?? "", /^ed25519:/);
177
+ assert.equal(runner.tokenHash, undefined, "token hashes must not be exposed");
178
+ assert.equal(runner.publicKeyPem, undefined, "raw PEM is not part of the summary");
179
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,259 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { test } from "node:test";
6
+ import { generateEd25519KeyPair } from "@fusionkit/protocol";
7
+ import { generateMasterKeyHex, masterKeyFromMaterial, open, seal } from "../keys.js";
8
+ import { Plane } from "../plane.js";
9
+ import { defaultPolicy } from "../policy.js";
10
+ import { RateLimiter } from "../ratelimit.js";
11
+ import { SecretStore } from "../secrets.js";
12
+ import { SqliteStore } from "../sqlite-store.js";
13
+ function manifest() {
14
+ return {
15
+ version: "warrant.manifest.v1",
16
+ baseRef: "0".repeat(40),
17
+ bundleHash: "1".repeat(64),
18
+ untrackedFiles: [],
19
+ deniedPatterns: [],
20
+ deniedPaths: []
21
+ };
22
+ }
23
+ function runRequest(pool) {
24
+ return {
25
+ requestedBy: { kind: "human", id: "tester" },
26
+ agentKind: "mock",
27
+ prompt: "hardening",
28
+ pool,
29
+ secretNames: [],
30
+ workspace: manifest(),
31
+ network: { defaultDeny: true, allowHosts: [] },
32
+ budget: {},
33
+ disclosure: "minimal-context"
34
+ };
35
+ }
36
+ function makePlane(options = {}) {
37
+ const dir = options.dataDir ?? mkdtempSync(join(tmpdir(), "warrant-hard-"));
38
+ const keys = generateEd25519KeyPair();
39
+ const policy = defaultPolicy();
40
+ const plane = new Plane({
41
+ dataDir: dir,
42
+ policy,
43
+ planePrivateKeyPem: keys.privateKeyPem,
44
+ planePublicKeyPem: keys.publicKeyPem,
45
+ adminToken: "admin-tok",
46
+ enrollToken: "enroll-tok",
47
+ secretStore: new SecretStore(join(dir, "secrets.enc"), masterKeyFromMaterial(generateMasterKeyHex()))
48
+ });
49
+ return {
50
+ plane,
51
+ dir,
52
+ stop: () => {
53
+ plane.close();
54
+ rmSync(dir, { recursive: true, force: true });
55
+ }
56
+ };
57
+ }
58
+ test("atomic claim: each created run is claimed exactly once", () => {
59
+ const { plane, stop } = makePlane();
60
+ try {
61
+ const a = plane.enrollRunner({
62
+ enrollToken: "enroll-tok",
63
+ publicKeyPem: generateEd25519KeyPair().publicKeyPem,
64
+ pool: "default"
65
+ });
66
+ const b = plane.enrollRunner({
67
+ enrollToken: "enroll-tok",
68
+ publicKeyPem: generateEd25519KeyPair().publicKeyPem,
69
+ pool: "default"
70
+ });
71
+ const runIds = new Set();
72
+ for (let i = 0; i < 20; i++) {
73
+ runIds.add(plane.requestRun(runRequest("default")).id);
74
+ }
75
+ // Two runners drain the queue; every claim must be a distinct run, and
76
+ // the total claimed must equal the number created (no double-claim, no loss).
77
+ const claimed = [];
78
+ for (;;) {
79
+ const fromA = plane.claim({ runnerToken: a.runnerToken, pool: "default" });
80
+ const fromB = plane.claim({ runnerToken: b.runnerToken, pool: "default" });
81
+ if (fromA)
82
+ claimed.push(fromA.runId);
83
+ if (fromB)
84
+ claimed.push(fromB.runId);
85
+ if (!fromA && !fromB)
86
+ break;
87
+ }
88
+ assert.equal(claimed.length, runIds.size);
89
+ assert.equal(new Set(claimed).size, claimed.length, "no run was claimed twice");
90
+ for (const id of claimed)
91
+ assert.ok(runIds.has(id));
92
+ }
93
+ finally {
94
+ stop();
95
+ }
96
+ });
97
+ test("plane state (runs, runners) is durable across a restart", () => {
98
+ const dir = mkdtempSync(join(tmpdir(), "warrant-restart-"));
99
+ let runId;
100
+ try {
101
+ const first = makePlane({ dataDir: dir });
102
+ first.plane.enrollRunner({
103
+ enrollToken: "enroll-tok",
104
+ publicKeyPem: generateEd25519KeyPair().publicKeyPem,
105
+ pool: "default"
106
+ });
107
+ runId = first.plane.requestRun(runRequest("default")).id;
108
+ first.plane.close();
109
+ // Reopen against the same database: a brand-new plane instance sees the
110
+ // run and the enrolled runner. (The nonce ledger lives in this same DB,
111
+ // which is what makes completion replay protection survive restarts.)
112
+ const second = makePlane({ dataDir: dir });
113
+ try {
114
+ assert.equal(second.plane.getRun(runId)?.status, "created");
115
+ assert.ok(second.plane.listRunners().length >= 1);
116
+ }
117
+ finally {
118
+ second.plane.close();
119
+ }
120
+ }
121
+ finally {
122
+ rmSync(dir, { recursive: true, force: true });
123
+ }
124
+ });
125
+ test("durable nonce ledger rejects a replayed nonce, even after reopen", () => {
126
+ const dir = mkdtempSync(join(tmpdir(), "warrant-nonce-"));
127
+ try {
128
+ const store1 = new SqliteStore(join(dir, "plane.db"));
129
+ assert.equal(store1.recordClaimNonce("nonce-1", Date.now() + 100000), true);
130
+ assert.equal(store1.recordClaimNonce("nonce-1", Date.now() + 100000), false);
131
+ store1.close();
132
+ // Reopen: the nonce is still present and still rejected.
133
+ const store2 = new SqliteStore(join(dir, "plane.db"));
134
+ assert.equal(store2.recordClaimNonce("nonce-1", Date.now() + 100000), false);
135
+ // Pruning removes expired nonces.
136
+ assert.equal(store2.recordClaimNonce("nonce-2", Date.now() - 1), true);
137
+ assert.ok(store2.pruneClaimNonces(Date.now()) >= 1);
138
+ store2.close();
139
+ }
140
+ finally {
141
+ rmSync(dir, { recursive: true, force: true });
142
+ }
143
+ });
144
+ test("principals: issue, role gating, rotation, and revocation", () => {
145
+ const { plane, stop } = makePlane();
146
+ try {
147
+ assert.equal(plane.authenticate("admin-tok")?.role, "admin");
148
+ assert.equal(plane.checkAdminToken("admin-tok"), true);
149
+ assert.equal(plane.authenticate("nope"), undefined);
150
+ const requester = plane.issuePrincipal("ci-bot", "requester");
151
+ const p = plane.authenticate(requester.token);
152
+ assert.equal(p?.role, "requester");
153
+ // requester can create runs but not manage principals.
154
+ assert.ok(plane.authorize(requester.token, "runs:create"));
155
+ assert.equal(plane.authorize(requester.token, "principals:manage"), undefined);
156
+ // Rotation invalidates the old token.
157
+ const rotated = plane.rotatePrincipal("ci-bot");
158
+ assert.equal(plane.authenticate(requester.token), undefined);
159
+ assert.equal(plane.authenticate(rotated.token)?.name, "ci-bot");
160
+ // Revocation invalidates entirely.
161
+ assert.equal(plane.revokePrincipal("ci-bot"), true);
162
+ assert.equal(plane.authenticate(rotated.token), undefined);
163
+ assert.throws(() => plane.issuePrincipal("admin", "admin"), /already exists/);
164
+ }
165
+ finally {
166
+ stop();
167
+ }
168
+ });
169
+ test("single-use enroll tokens are consumed exactly once and expire", () => {
170
+ const { plane, stop } = makePlane();
171
+ try {
172
+ const issued = plane.issueEnrollToken({ pool: "default" });
173
+ const first = plane.enrollRunner({
174
+ enrollToken: issued.token,
175
+ publicKeyPem: generateEd25519KeyPair().publicKeyPem,
176
+ pool: "default"
177
+ });
178
+ assert.match(first.runnerId, /^rnr_/);
179
+ // Second use of the same single-use token is rejected.
180
+ assert.throws(() => plane.enrollRunner({
181
+ enrollToken: issued.token,
182
+ publicKeyPem: generateEd25519KeyPair().publicKeyPem,
183
+ pool: "default"
184
+ }), /invalid enroll token/);
185
+ // Expired token is rejected.
186
+ const expired = plane.issueEnrollToken({ pool: "default", ttlMs: -1 });
187
+ assert.throws(() => plane.enrollRunner({
188
+ enrollToken: expired.token,
189
+ publicKeyPem: generateEd25519KeyPair().publicKeyPem,
190
+ pool: "default"
191
+ }), /invalid enroll token/);
192
+ }
193
+ finally {
194
+ stop();
195
+ }
196
+ });
197
+ test("rate limiter: token bucket and auth-failure lockout", () => {
198
+ let nowMs = 1_000_000;
199
+ const limiter = new RateLimiter({ ratePerSec: 1, burst: 3, authFailureLimit: 3, authFailureWindowMs: 1000 }, () => nowMs);
200
+ assert.equal(limiter.allow("k"), true);
201
+ assert.equal(limiter.allow("k"), true);
202
+ assert.equal(limiter.allow("k"), true);
203
+ assert.equal(limiter.allow("k"), false, "burst exhausted");
204
+ nowMs += 1100; // refill ~1 token
205
+ assert.equal(limiter.allow("k"), true);
206
+ assert.equal(limiter.isLockedOut("ip"), false);
207
+ limiter.recordAuthFailure("ip");
208
+ limiter.recordAuthFailure("ip");
209
+ assert.equal(limiter.isLockedOut("ip"), false);
210
+ limiter.recordAuthFailure("ip");
211
+ assert.equal(limiter.isLockedOut("ip"), true, "locked out after the limit");
212
+ limiter.recordAuthSuccess("ip");
213
+ assert.equal(limiter.isLockedOut("ip"), false, "success clears the lockout");
214
+ });
215
+ test("master-key sealing round-trips and rejects the wrong key", () => {
216
+ const master = masterKeyFromMaterial(generateMasterKeyHex());
217
+ const sealed = seal(master, Buffer.from("top secret", "utf8"));
218
+ assert.equal(open(master, sealed).toString("utf8"), "top secret");
219
+ const other = masterKeyFromMaterial(generateMasterKeyHex());
220
+ assert.throws(() => open(other, sealed));
221
+ // The sealed blob does not contain the plaintext.
222
+ assert.ok(!JSON.stringify(sealed).includes("top secret"));
223
+ });
224
+ test("secret store seals at rest and the master key is required to read", () => {
225
+ const dir = mkdtempSync(join(tmpdir(), "warrant-secret-"));
226
+ try {
227
+ const master = masterKeyFromMaterial(generateMasterKeyHex());
228
+ const store = new SecretStore(join(dir, "secrets.enc"), master);
229
+ store.set("API_KEY", "sk-live-do-not-leak");
230
+ assert.deepEqual(store.release(["API_KEY"]), [
231
+ { name: "API_KEY", value: "sk-live-do-not-leak" }
232
+ ]);
233
+ // A store opened with a different master key cannot read.
234
+ const wrong = new SecretStore(join(dir, "secrets.enc"), masterKeyFromMaterial(generateMasterKeyHex()));
235
+ assert.throws(() => wrong.names());
236
+ store.rotate("API_KEY", "sk-live-rotated");
237
+ assert.equal(store.release(["API_KEY"])[0]?.value, "sk-live-rotated");
238
+ assert.equal(store.remove("API_KEY"), true);
239
+ assert.throws(() => store.release(["API_KEY"]), /not in the store/);
240
+ }
241
+ finally {
242
+ rmSync(dir, { recursive: true, force: true });
243
+ }
244
+ });
245
+ test("retention sweep deletes old terminal runs and GCs unreferenced blobs", () => {
246
+ const { plane, stop } = makePlane();
247
+ try {
248
+ // An orphan blob nothing references should be collected.
249
+ const orphan = plane.blobs.putBlob(Buffer.from("orphan", "utf8"));
250
+ // A referenced workspace bundle blob should survive.
251
+ plane.requestRun(runRequest("default"));
252
+ const result = plane.sweepRetention();
253
+ assert.ok(result.deletedBlobs >= 1, "orphan blob collected");
254
+ assert.equal(plane.blobs.getBlob(orphan), undefined);
255
+ }
256
+ finally {
257
+ stop();
258
+ }
259
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,78 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { defaultPolicy, evaluatePolicy } from "../policy.js";
4
+ import { PolicyDeniedError } from "@fusionkit/protocol";
5
+ function policyFixture() {
6
+ const policy = defaultPolicy();
7
+ policy.runners.allowPools = ["eng-prod"];
8
+ policy.agents.allow = ["mock", "claude-code"];
9
+ policy.network.allowHosts = ["registry.npmjs.org"];
10
+ policy.secrets.releasable = [
11
+ { name: "MOCK_SECRET", scope: "test", pools: ["eng-prod"] }
12
+ ];
13
+ return policy;
14
+ }
15
+ test("policy allows a compliant run", () => {
16
+ const decision = evaluatePolicy(policyFixture(), {
17
+ agentKind: "mock",
18
+ pool: "eng-prod",
19
+ secretNames: ["MOCK_SECRET"],
20
+ allowHosts: ["registry.npmjs.org"]
21
+ });
22
+ assert.equal(decision.decision, "allow");
23
+ });
24
+ test("policy fails closed on disallowed agent, pool, secret, host, and budget", () => {
25
+ const policy = policyFixture();
26
+ assert.throws(() => evaluatePolicy(policy, {
27
+ agentKind: "codex",
28
+ pool: "eng-prod",
29
+ secretNames: [],
30
+ allowHosts: []
31
+ }), PolicyDeniedError);
32
+ assert.throws(() => evaluatePolicy(policy, {
33
+ agentKind: "mock",
34
+ pool: "other-pool",
35
+ secretNames: [],
36
+ allowHosts: []
37
+ }), PolicyDeniedError);
38
+ assert.throws(() => evaluatePolicy(policy, {
39
+ agentKind: "mock",
40
+ pool: "eng-prod",
41
+ secretNames: ["UNKNOWN_SECRET"],
42
+ allowHosts: []
43
+ }), PolicyDeniedError);
44
+ assert.throws(() => evaluatePolicy(policy, {
45
+ agentKind: "mock",
46
+ pool: "eng-prod",
47
+ secretNames: [],
48
+ allowHosts: ["exfil.example.com"]
49
+ }), PolicyDeniedError);
50
+ assert.throws(() => evaluatePolicy(policy, {
51
+ agentKind: "mock",
52
+ pool: "eng-prod",
53
+ secretNames: [],
54
+ allowHosts: [],
55
+ maxSpendUsd: 10_000
56
+ }), PolicyDeniedError);
57
+ });
58
+ test("policy returns ask when a consent rule matches", () => {
59
+ const policy = policyFixture();
60
+ policy.consent = [{ when: "secret-release", approvers: ["security-team"] }];
61
+ const withSecret = evaluatePolicy(policy, {
62
+ agentKind: "mock",
63
+ pool: "eng-prod",
64
+ secretNames: ["MOCK_SECRET"],
65
+ allowHosts: []
66
+ });
67
+ assert.equal(withSecret.decision, "ask");
68
+ assert.deepEqual(withSecret.consentRequirements, [
69
+ "secret-release:MOCK_SECRET"
70
+ ]);
71
+ const withoutSecret = evaluatePolicy(policy, {
72
+ agentKind: "mock",
73
+ pool: "eng-prod",
74
+ secretNames: [],
75
+ allowHosts: []
76
+ });
77
+ assert.equal(withoutSecret.decision, "allow");
78
+ });
@@ -0,0 +1 @@
1
+ export {};