macro-agent 0.1.12 → 0.2.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.
- package/dist/agent/agent-manager-v2.d.ts.map +1 -1
- package/dist/agent/agent-manager-v2.js +240 -7
- package/dist/agent/agent-manager-v2.js.map +1 -1
- package/dist/agent/types.d.ts +47 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/boot-v2.d.ts +33 -0
- package/dist/boot-v2.d.ts.map +1 -1
- package/dist/boot-v2.js +142 -11
- package/dist/boot-v2.js.map +1 -1
- package/dist/cli/inbox-mcp-proxy.d.ts +36 -0
- package/dist/cli/inbox-mcp-proxy.d.ts.map +1 -0
- package/dist/cli/inbox-mcp-proxy.js +51 -0
- package/dist/cli/inbox-mcp-proxy.js.map +1 -0
- package/dist/dispatch/loadout-translation.d.ts +100 -0
- package/dist/dispatch/loadout-translation.d.ts.map +1 -0
- package/dist/dispatch/loadout-translation.js +90 -0
- package/dist/dispatch/loadout-translation.js.map +1 -0
- package/dist/dispatch/mail-inbound-consumer.d.ts +89 -0
- package/dist/dispatch/mail-inbound-consumer.d.ts.map +1 -0
- package/dist/dispatch/mail-inbound-consumer.js +261 -0
- package/dist/dispatch/mail-inbound-consumer.js.map +1 -0
- package/dist/dispatch/mail-inbound-reuse-consumer.d.ts +75 -0
- package/dist/dispatch/mail-inbound-reuse-consumer.d.ts.map +1 -0
- package/dist/dispatch/mail-inbound-reuse-consumer.js +325 -0
- package/dist/dispatch/mail-inbound-reuse-consumer.js.map +1 -0
- package/dist/dispatch/permission-evaluator.d.ts +68 -0
- package/dist/dispatch/permission-evaluator.d.ts.map +1 -0
- package/dist/dispatch/permission-evaluator.js +159 -0
- package/dist/dispatch/permission-evaluator.js.map +1 -0
- package/dist/dispatch/permission-overlay.d.ts +64 -0
- package/dist/dispatch/permission-overlay.d.ts.map +1 -0
- package/dist/dispatch/permission-overlay.js +72 -0
- package/dist/dispatch/permission-overlay.js.map +1 -0
- package/dist/dispatch/permissions-handler.d.ts +71 -0
- package/dist/dispatch/permissions-handler.d.ts.map +1 -0
- package/dist/dispatch/permissions-handler.js +83 -0
- package/dist/dispatch/permissions-handler.js.map +1 -0
- package/dist/dispatch/spawn-agent-handler.d.ts +84 -0
- package/dist/dispatch/spawn-agent-handler.d.ts.map +1 -0
- package/dist/dispatch/spawn-agent-handler.js +85 -0
- package/dist/dispatch/spawn-agent-handler.js.map +1 -0
- package/dist/lifecycle/handlers-v2.d.ts +7 -0
- package/dist/lifecycle/handlers-v2.d.ts.map +1 -1
- package/dist/lifecycle/handlers-v2.js +27 -0
- package/dist/lifecycle/handlers-v2.js.map +1 -1
- package/dist/map/lifecycle-bridge.d.ts +18 -0
- package/dist/map/lifecycle-bridge.d.ts.map +1 -1
- package/dist/map/lifecycle-bridge.js +23 -1
- package/dist/map/lifecycle-bridge.js.map +1 -1
- package/dist/map/mail-bridge.d.ts +55 -0
- package/dist/map/mail-bridge.d.ts.map +1 -0
- package/dist/map/mail-bridge.js +115 -0
- package/dist/map/mail-bridge.js.map +1 -0
- package/dist/map/sidecar.d.ts.map +1 -1
- package/dist/map/sidecar.js +245 -1
- package/dist/map/sidecar.js.map +1 -1
- package/dist/map/types.d.ts +15 -0
- package/dist/map/types.d.ts.map +1 -1
- package/dist/mcp/tools/done-v2.d.ts.map +1 -1
- package/dist/mcp/tools/done-v2.js +1 -0
- package/dist/mcp/tools/done-v2.js.map +1 -1
- package/dist/teams/team-loader.d.ts.map +1 -1
- package/dist/teams/team-loader.js.map +1 -1
- package/dist/teams/team-runtime-v2.d.ts.map +1 -1
- package/dist/teams/team-runtime-v2.js +2 -0
- package/dist/teams/team-runtime-v2.js.map +1 -1
- package/package.json +6 -5
- package/src/agent/__tests__/agent-manager-v2.permission-interception.test.ts +296 -0
- package/src/agent/__tests__/agent-manager-v2.permissions.test.ts +233 -0
- package/src/agent/agent-manager-v2.ts +268 -8
- package/src/agent/types.ts +51 -0
- package/src/boot-v2.ts +190 -12
- package/src/cli/inbox-mcp-proxy.ts +56 -0
- package/src/dispatch/CLAUDE.md +129 -0
- package/src/dispatch/__tests__/loadout-translation.test.ts +141 -0
- package/src/dispatch/__tests__/mail-inbound-consumer.integration.test.ts +519 -0
- package/src/dispatch/__tests__/mail-inbound-consumer.test.ts +589 -0
- package/src/dispatch/__tests__/mail-inbound-reuse-consumer.test.ts +575 -0
- package/src/dispatch/__tests__/permission-evaluator.test.ts +196 -0
- package/src/dispatch/__tests__/permission-overlay.test.ts +56 -0
- package/src/dispatch/__tests__/permissions-handler.test.ts +168 -0
- package/src/dispatch/__tests__/spawn-agent-handler.test.ts +282 -0
- package/src/dispatch/loadout-translation.ts +138 -0
- package/src/dispatch/mail-inbound-consumer.ts +397 -0
- package/src/dispatch/mail-inbound-reuse-consumer.ts +479 -0
- package/src/dispatch/permission-evaluator.ts +191 -0
- package/src/dispatch/permission-overlay.ts +89 -0
- package/src/dispatch/permissions-handler.ts +112 -0
- package/src/dispatch/spawn-agent-handler.ts +160 -0
- package/src/lifecycle/handlers-v2.ts +34 -0
- package/src/map/__tests__/lifecycle-bridge.test.ts +64 -0
- package/src/map/__tests__/mail-bridge.test.ts +196 -0
- package/src/map/lifecycle-bridge.ts +48 -2
- package/src/map/mail-bridge.ts +203 -0
- package/src/map/sidecar.ts +346 -1
- package/src/map/types.ts +21 -0
- package/src/mcp/tools/done-v2.ts +1 -0
- package/src/teams/team-loader.ts +3 -1
- package/src/teams/team-runtime-v2.ts +2 -0
- package/dist/workspace/dataplane-adapter.d.ts +0 -260
- package/dist/workspace/dataplane-adapter.d.ts.map +0 -1
- package/dist/workspace/dataplane-adapter.js +0 -416
- package/dist/workspace/dataplane-adapter.js.map +0 -1
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for `evaluatePermission` — the per-tool-call decision
|
|
3
|
+
* function used by the PreToolUse hook to enforce dispatch-supplied
|
|
4
|
+
* loadout permissions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from "vitest";
|
|
8
|
+
import {
|
|
9
|
+
evaluatePermission,
|
|
10
|
+
matchRule,
|
|
11
|
+
} from "../permission-evaluator.js";
|
|
12
|
+
import type { OverlayPermissions } from "../permission-overlay.js";
|
|
13
|
+
|
|
14
|
+
// ────────────────────────────────────────────────────────────────────
|
|
15
|
+
// matchRule — single-rule pattern matching
|
|
16
|
+
// ────────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
describe("matchRule", () => {
|
|
19
|
+
it("matches a bare rule (no parens) against any tool input", () => {
|
|
20
|
+
expect(matchRule("Bash", "Bash", { command: "anything" }).matched).toBe(true);
|
|
21
|
+
expect(matchRule("Bash", "Bash", { command: "" }).matched).toBe(true);
|
|
22
|
+
expect(matchRule("Read", "Bash", { command: "x" }).matched).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("matches a rule with empty parens (Bash()) — same as bare", () => {
|
|
26
|
+
expect(matchRule("Bash()", "Bash", { command: "echo hi" }).matched).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("matches a literal pattern against the primary input field", () => {
|
|
30
|
+
expect(
|
|
31
|
+
matchRule("Bash(rm -rf /tmp/foo)", "Bash", { command: "rm -rf /tmp/foo" }).matched,
|
|
32
|
+
).toBe(true);
|
|
33
|
+
expect(
|
|
34
|
+
matchRule("Bash(rm -rf /tmp/foo)", "Bash", { command: "rm -rf /tmp/bar" }).matched,
|
|
35
|
+
).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("treats `*` as glob (any sequence)", () => {
|
|
39
|
+
expect(
|
|
40
|
+
matchRule("Bash(echo perm-deny-test:*)", "Bash", {
|
|
41
|
+
command: "echo perm-deny-test:hello-world",
|
|
42
|
+
}).matched,
|
|
43
|
+
).toBe(true);
|
|
44
|
+
expect(
|
|
45
|
+
matchRule("Bash(echo perm-deny-test:*)", "Bash", {
|
|
46
|
+
command: "echo perm-allow-test:other",
|
|
47
|
+
}).matched,
|
|
48
|
+
).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("treats multiple `*` as multiple globs", () => {
|
|
52
|
+
expect(
|
|
53
|
+
matchRule("Bash(*node *.js)", "Bash", { command: "/usr/bin/node script.js" }).matched,
|
|
54
|
+
).toBe(true);
|
|
55
|
+
expect(
|
|
56
|
+
matchRule("Bash(*node *.js)", "Bash", { command: "/usr/bin/python script.py" }).matched,
|
|
57
|
+
).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("escapes regex specials in the pattern", () => {
|
|
61
|
+
// `.` is a regex special; should match literally, not as "any char".
|
|
62
|
+
expect(
|
|
63
|
+
matchRule("Write(.env)", "Write", { file_path: ".env" }).matched,
|
|
64
|
+
).toBe(true);
|
|
65
|
+
// Without escaping, `.` would match `aenv` too — but this MUST not match:
|
|
66
|
+
expect(
|
|
67
|
+
matchRule("Write(.env)", "Write", { file_path: "aenv" }).matched,
|
|
68
|
+
).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("matches `**` like single `*` (no path-segment distinction)", () => {
|
|
72
|
+
expect(
|
|
73
|
+
matchRule("Read(**)", "Read", { file_path: "/etc/passwd" }).matched,
|
|
74
|
+
).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("returns no match when the tool name doesn't match the rule's tool", () => {
|
|
78
|
+
expect(matchRule("Bash(*)", "Read", { file_path: "/x" }).matched).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("uses the right primary field per tool", () => {
|
|
82
|
+
expect(matchRule("Read(/etc/*)", "Read", { file_path: "/etc/passwd" }).field).toBe("file_path");
|
|
83
|
+
expect(matchRule("Bash(*)", "Bash", { command: "x" }).field).toBe("command");
|
|
84
|
+
expect(matchRule("Grep(TODO)", "Grep", { pattern: "TODO" }).field).toBe("pattern");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("returns no match for tools without a defined primary field when a pattern is given", () => {
|
|
88
|
+
// FakeTool has no PRIMARY_INPUT_FIELD entry; pattern-bearing rules
|
|
89
|
+
// can't match. Bare `FakeTool` rules WOULD still match (covered by
|
|
90
|
+
// the first test).
|
|
91
|
+
expect(matchRule("FakeTool(*)", "FakeTool", { x: "y" }).matched).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("returns no match when the input doesn't have the expected field", () => {
|
|
95
|
+
expect(matchRule("Bash(*)", "Bash", {}).matched).toBe(false);
|
|
96
|
+
expect(matchRule("Bash(*)", "Bash", { command: 42 }).matched).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("returns no match for malformed rules", () => {
|
|
100
|
+
expect(matchRule("", "Bash", { command: "x" }).matched).toBe(false);
|
|
101
|
+
expect(matchRule("(", "Bash", { command: "x" }).matched).toBe(false);
|
|
102
|
+
expect(matchRule("Bash(unclosed", "Bash", { command: "x" }).matched).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ────────────────────────────────────────────────────────────────────
|
|
107
|
+
// evaluatePermission — overlay-level decision precedence
|
|
108
|
+
// ────────────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
describe("evaluatePermission", () => {
|
|
111
|
+
it("returns 'pass-through' when overlay has no rules", () => {
|
|
112
|
+
expect(
|
|
113
|
+
evaluatePermission("Bash", { command: "echo" }, {}).decision,
|
|
114
|
+
).toBe("pass-through");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("returns 'pass-through' when no rule matches the call", () => {
|
|
118
|
+
const overlay: OverlayPermissions = {
|
|
119
|
+
deny: ["Bash(rm -rf:*)"],
|
|
120
|
+
allow: ["Read(**)"],
|
|
121
|
+
};
|
|
122
|
+
expect(
|
|
123
|
+
evaluatePermission("Write", { file_path: "/tmp/x" }, overlay).decision,
|
|
124
|
+
).toBe("pass-through");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("returns 'deny' when a deny rule matches", () => {
|
|
128
|
+
const overlay: OverlayPermissions = {
|
|
129
|
+
deny: ["Bash(echo perm-deny-test:*)"],
|
|
130
|
+
};
|
|
131
|
+
const result = evaluatePermission(
|
|
132
|
+
"Bash",
|
|
133
|
+
{ command: "echo perm-deny-test:hello" },
|
|
134
|
+
overlay,
|
|
135
|
+
);
|
|
136
|
+
expect(result.decision).toBe("deny");
|
|
137
|
+
expect(result.matchedRule).toBe("Bash(echo perm-deny-test:*)");
|
|
138
|
+
expect(result.matchedField).toBe("command");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("returns 'allow' when an allow rule matches and no deny matches", () => {
|
|
142
|
+
const overlay: OverlayPermissions = {
|
|
143
|
+
allow: ["Read(/safe/**)"],
|
|
144
|
+
deny: ["Bash(*)"],
|
|
145
|
+
};
|
|
146
|
+
const result = evaluatePermission(
|
|
147
|
+
"Read",
|
|
148
|
+
{ file_path: "/safe/foo.txt" },
|
|
149
|
+
overlay,
|
|
150
|
+
);
|
|
151
|
+
expect(result.decision).toBe("allow");
|
|
152
|
+
expect(result.matchedRule).toBe("Read(/safe/**)");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("deny takes precedence over allow when both match", () => {
|
|
156
|
+
const overlay: OverlayPermissions = {
|
|
157
|
+
// Pattern uses `*` glob to cover the suffix; the colon convention
|
|
158
|
+
// (`Bash(rm -rf:*)`) is also legal but the test exercises the
|
|
159
|
+
// simpler suffix-glob form against a real space-separated command.
|
|
160
|
+
deny: ["Bash(rm -rf*)"],
|
|
161
|
+
allow: ["Bash(*)"],
|
|
162
|
+
};
|
|
163
|
+
expect(
|
|
164
|
+
evaluatePermission("Bash", { command: "rm -rf /tmp" }, overlay).decision,
|
|
165
|
+
).toBe("deny");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("first matching deny rule wins (matchedRule reports it)", () => {
|
|
169
|
+
const overlay: OverlayPermissions = {
|
|
170
|
+
deny: ["Bash(echo *)", "Bash(*)"],
|
|
171
|
+
};
|
|
172
|
+
const result = evaluatePermission(
|
|
173
|
+
"Bash",
|
|
174
|
+
{ command: "echo hi" },
|
|
175
|
+
overlay,
|
|
176
|
+
);
|
|
177
|
+
expect(result.decision).toBe("deny");
|
|
178
|
+
expect(result.matchedRule).toBe("Bash(echo *)");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("matches against the live test's exact rule shape (regression target)", () => {
|
|
182
|
+
// This is the exact rule the live-mail-reuse-dispatch.test.ts ships
|
|
183
|
+
// on the wire; the evaluator MUST match it for the live test's
|
|
184
|
+
// PERM_DENIED assertion to pass.
|
|
185
|
+
const overlay: OverlayPermissions = {
|
|
186
|
+
deny: ["Bash(echo perm-deny-test:*)"],
|
|
187
|
+
};
|
|
188
|
+
expect(
|
|
189
|
+
evaluatePermission(
|
|
190
|
+
"Bash",
|
|
191
|
+
{ command: "echo perm-deny-test:hello-world" },
|
|
192
|
+
overlay,
|
|
193
|
+
).decision,
|
|
194
|
+
).toBe("deny");
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the per-process permission-overlay registry.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
6
|
+
import {
|
|
7
|
+
setPermissionOverlay,
|
|
8
|
+
clearPermissionOverlay,
|
|
9
|
+
getPermissionOverlay,
|
|
10
|
+
clearAllPermissionOverlays,
|
|
11
|
+
_resetForTest,
|
|
12
|
+
_sizeForTest,
|
|
13
|
+
} from "../permission-overlay.js";
|
|
14
|
+
|
|
15
|
+
describe("permission-overlay registry", () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
_resetForTest();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("returns undefined when no overlay is set", () => {
|
|
21
|
+
expect(getPermissionOverlay("agent-A")).toBeUndefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("set / get round-trip", () => {
|
|
25
|
+
setPermissionOverlay("agent-A", { deny: ["Bash(rm:*)"] });
|
|
26
|
+
expect(getPermissionOverlay("agent-A")).toEqual({ deny: ["Bash(rm:*)"] });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("clear removes only the targeted agent's overlay", () => {
|
|
30
|
+
setPermissionOverlay("agent-A", { deny: ["Bash(*)"] });
|
|
31
|
+
setPermissionOverlay("agent-B", { deny: ["Read(*)"] });
|
|
32
|
+
clearPermissionOverlay("agent-A");
|
|
33
|
+
expect(getPermissionOverlay("agent-A")).toBeUndefined();
|
|
34
|
+
expect(getPermissionOverlay("agent-B")).toEqual({ deny: ["Read(*)"] });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("clear is idempotent (no error when no overlay)", () => {
|
|
38
|
+
expect(() => clearPermissionOverlay("ghost")).not.toThrow();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("set overwrites prior overlay for the same agent", () => {
|
|
42
|
+
setPermissionOverlay("agent-A", { deny: ["Bash(*)"] });
|
|
43
|
+
setPermissionOverlay("agent-A", { allow: ["Read(*)"] });
|
|
44
|
+
expect(getPermissionOverlay("agent-A")).toEqual({ allow: ["Read(*)"] });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("clearAllPermissionOverlays drops everything", () => {
|
|
48
|
+
setPermissionOverlay("a", { deny: ["x"] });
|
|
49
|
+
setPermissionOverlay("b", { deny: ["y"] });
|
|
50
|
+
setPermissionOverlay("c", { deny: ["z"] });
|
|
51
|
+
expect(_sizeForTest()).toBe(3);
|
|
52
|
+
clearAllPermissionOverlays();
|
|
53
|
+
expect(_sizeForTest()).toBe(0);
|
|
54
|
+
expect(getPermissionOverlay("a")).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the `x-dispatch/permissions.{set,clear}` MAP request
|
|
3
|
+
* handlers. These verify the wire-shape contract — the OpenHive ACP+reuse
|
|
4
|
+
* dispatch path calls these to inject loadout deny rules into a long-lived
|
|
5
|
+
* agent's permission overlay before driving the prompt, and to clear them
|
|
6
|
+
* after the dispatch completes.
|
|
7
|
+
*
|
|
8
|
+
* The actual enforcement is exercised end-to-end in the live test
|
|
9
|
+
* (live-acp-reuse-dispatch.test.ts); here we just pin the handler's
|
|
10
|
+
* input/output contract and the side effects on the overlay registry.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
14
|
+
import {
|
|
15
|
+
handlePermissionsSet,
|
|
16
|
+
handlePermissionsClear,
|
|
17
|
+
X_DISPATCH_PERMISSIONS_METHODS,
|
|
18
|
+
} from "../permissions-handler.js";
|
|
19
|
+
import {
|
|
20
|
+
getPermissionOverlay,
|
|
21
|
+
clearPermissionOverlay,
|
|
22
|
+
} from "../permission-overlay.js";
|
|
23
|
+
|
|
24
|
+
describe("x-dispatch/permissions handlers", () => {
|
|
25
|
+
const TEST_AGENT_ID = "agent_test_perm_handler_xyz";
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
clearPermissionOverlay(TEST_AGENT_ID);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
clearPermissionOverlay(TEST_AGENT_ID);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("set", () => {
|
|
36
|
+
it("accepts deny + allow, writes overlay, returns ok=true", () => {
|
|
37
|
+
const result = handlePermissionsSet(
|
|
38
|
+
{
|
|
39
|
+
agent_id: TEST_AGENT_ID,
|
|
40
|
+
deny: ["Read(/etc/secrets)"],
|
|
41
|
+
allow: ["Read(/tmp/*)"],
|
|
42
|
+
},
|
|
43
|
+
() => {},
|
|
44
|
+
);
|
|
45
|
+
expect(result).toEqual({ ok: true });
|
|
46
|
+
const overlay = getPermissionOverlay(TEST_AGENT_ID);
|
|
47
|
+
expect(overlay).toEqual({
|
|
48
|
+
deny: ["Read(/etc/secrets)"],
|
|
49
|
+
allow: ["Read(/tmp/*)"],
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("accepts deny only", () => {
|
|
54
|
+
const result = handlePermissionsSet(
|
|
55
|
+
{ agent_id: TEST_AGENT_ID, deny: ["Bash(rm -rf:*)"] },
|
|
56
|
+
() => {},
|
|
57
|
+
);
|
|
58
|
+
expect(result).toEqual({ ok: true });
|
|
59
|
+
expect(getPermissionOverlay(TEST_AGENT_ID)).toEqual({
|
|
60
|
+
deny: ["Bash(rm -rf:*)"],
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("accepts allow only", () => {
|
|
65
|
+
const result = handlePermissionsSet(
|
|
66
|
+
{ agent_id: TEST_AGENT_ID, allow: ["Read(*)"] },
|
|
67
|
+
() => {},
|
|
68
|
+
);
|
|
69
|
+
expect(result).toEqual({ ok: true });
|
|
70
|
+
expect(getPermissionOverlay(TEST_AGENT_ID)).toEqual({
|
|
71
|
+
allow: ["Read(*)"],
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("re-setting overwrites the prior overlay (idempotent)", () => {
|
|
76
|
+
handlePermissionsSet(
|
|
77
|
+
{ agent_id: TEST_AGENT_ID, deny: ["Read(/old)"] },
|
|
78
|
+
() => {},
|
|
79
|
+
);
|
|
80
|
+
handlePermissionsSet(
|
|
81
|
+
{ agent_id: TEST_AGENT_ID, deny: ["Read(/new)"] },
|
|
82
|
+
() => {},
|
|
83
|
+
);
|
|
84
|
+
expect(getPermissionOverlay(TEST_AGENT_ID)).toEqual({
|
|
85
|
+
deny: ["Read(/new)"],
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("rejects missing agent_id", () => {
|
|
90
|
+
// @ts-expect-error - testing runtime validation
|
|
91
|
+
const result = handlePermissionsSet({ deny: [] }, () => {});
|
|
92
|
+
expect(result.ok).toBe(false);
|
|
93
|
+
expect((result as { error: string }).error).toMatch(/agent_id/);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("rejects non-string agent_id", () => {
|
|
97
|
+
const result = handlePermissionsSet(
|
|
98
|
+
// @ts-expect-error - testing runtime validation
|
|
99
|
+
{ agent_id: 42, deny: [] },
|
|
100
|
+
() => {},
|
|
101
|
+
);
|
|
102
|
+
expect(result.ok).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("rejects non-array deny", () => {
|
|
106
|
+
const result = handlePermissionsSet(
|
|
107
|
+
// @ts-expect-error - testing runtime validation
|
|
108
|
+
{ agent_id: TEST_AGENT_ID, deny: "Read(*)" },
|
|
109
|
+
() => {},
|
|
110
|
+
);
|
|
111
|
+
expect(result.ok).toBe(false);
|
|
112
|
+
expect((result as { error: string }).error).toMatch(/deny/);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("rejects non-array allow", () => {
|
|
116
|
+
const result = handlePermissionsSet(
|
|
117
|
+
// @ts-expect-error - testing runtime validation
|
|
118
|
+
{ agent_id: TEST_AGENT_ID, allow: { 0: "x" } },
|
|
119
|
+
() => {},
|
|
120
|
+
);
|
|
121
|
+
expect(result.ok).toBe(false);
|
|
122
|
+
expect((result as { error: string }).error).toMatch(/allow/);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("clear", () => {
|
|
127
|
+
it("removes a previously-set overlay", () => {
|
|
128
|
+
handlePermissionsSet(
|
|
129
|
+
{ agent_id: TEST_AGENT_ID, deny: ["Read(/secret)"] },
|
|
130
|
+
() => {},
|
|
131
|
+
);
|
|
132
|
+
expect(getPermissionOverlay(TEST_AGENT_ID)).toBeTruthy();
|
|
133
|
+
|
|
134
|
+
const result = handlePermissionsClear(
|
|
135
|
+
{ agent_id: TEST_AGENT_ID },
|
|
136
|
+
() => {},
|
|
137
|
+
);
|
|
138
|
+
expect(result).toEqual({ ok: true });
|
|
139
|
+
expect(getPermissionOverlay(TEST_AGENT_ID)).toBeUndefined();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("idempotent — clearing a non-existent overlay returns ok=true", () => {
|
|
143
|
+
const result = handlePermissionsClear(
|
|
144
|
+
{ agent_id: TEST_AGENT_ID },
|
|
145
|
+
() => {},
|
|
146
|
+
);
|
|
147
|
+
expect(result).toEqual({ ok: true });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("rejects missing agent_id", () => {
|
|
151
|
+
// @ts-expect-error - testing runtime validation
|
|
152
|
+
const result = handlePermissionsClear({}, () => {});
|
|
153
|
+
expect(result.ok).toBe(false);
|
|
154
|
+
expect((result as { error: string }).error).toMatch(/agent_id/);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("method names — pinning the wire contract", () => {
|
|
159
|
+
it("matches the namespaced x-dispatch/permissions.* convention", () => {
|
|
160
|
+
expect(X_DISPATCH_PERMISSIONS_METHODS).toEqual({
|
|
161
|
+
SET_REQUEST: "x-dispatch/permissions.set.request",
|
|
162
|
+
SET_RESPONSE: "x-dispatch/permissions.set.response",
|
|
163
|
+
CLEAR_REQUEST: "x-dispatch/permissions.clear.request",
|
|
164
|
+
CLEAR_RESPONSE: "x-dispatch/permissions.clear.response",
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|