mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 09:41:04 +01:00
test(plugins): cover the never-a-wildcard invariant for proactive scope resolution
Closes the Greptile P2 on #10103: referencedCompanyId's null path had no coverage. Two cases, one per derivation branch: - direct-companyId branch: a proactive call referencing no company reaches the gate scope-less (handler sees no injected companyId) - scopeKind:company branch: empty scopeId resolves to null, not to an authorized company — denied with 'company context is required', proving no grant occurred (a wrong grant would fail with a different message) Refs LOOA-629 / PR #10103 (LOOA-693 security review flagged the same gap). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J4onvauR8cQq9uo9iJbcZ7
This commit is contained in:
1 parent
3093c5e694
commit
b01c9e42b9
1 file changed
+45
@@ -617,6 +617,51 @@ describe("plugin proactive company scope (LOOA-629)", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("never injects a scope into a call that references no company (direct-companyId branch)", async () => {
|
||||
// The "never a wildcard" invariant (#10103 review gap), direct-companyId
|
||||
// derivation branch: a proactive call with no company reference must reach
|
||||
// the gate scope-less. The gate classifies it as instance-level
|
||||
// (kind "none") and passes params through verbatim — so the handler seeing
|
||||
// NO companyId proves the manager did not inject one of its authorized
|
||||
// companies into the call.
|
||||
const { handle, companiesGet } = makeHandle();
|
||||
try {
|
||||
await handle.start();
|
||||
handle.setProactiveCompanyScopes(["company-1"]);
|
||||
await handle.call("getData", {
|
||||
params: { mode: "omit", hostMethod: "companies.get" },
|
||||
} as unknown as HostToWorkerMethods["getData"][0]);
|
||||
expect(companiesGet).toHaveBeenCalledTimes(1);
|
||||
const receivedParams = companiesGet.mock.calls[0]?.[0] as Record<string, unknown> | undefined;
|
||||
expect(receivedParams?.companyId).toBeUndefined();
|
||||
} finally {
|
||||
await handle.stop().catch(() => undefined);
|
||||
}
|
||||
});
|
||||
|
||||
it("never grants a wildcard: company-scoped state.get with an empty scopeId stays denied scope-less", async () => {
|
||||
// Same invariant via the scopeKind:"company" derivation branch — an empty
|
||||
// scopeId must resolve to null (no proactive grant). The gate then
|
||||
// escalates the empty-scopeId request to kind "all", which is denied. The
|
||||
// message assertion is load-bearing: "company context is required" proves
|
||||
// NO scope was granted, whereas a wrongly-injected grant would fail with
|
||||
// 'the current invocation is scoped to company "company-1"' instead.
|
||||
const { handle, stateGet } = makeHandle();
|
||||
try {
|
||||
await handle.start();
|
||||
handle.setProactiveCompanyScopes(["company-1"]);
|
||||
await expect(handle.call("getData", {
|
||||
params: { mode: "omit", hostMethod: "state.get" },
|
||||
} as unknown as HostToWorkerMethods["getData"][0])).rejects.toMatchObject({
|
||||
code: PLUGIN_RPC_ERROR_CODES.INVOCATION_SCOPE_DENIED,
|
||||
message: expect.stringContaining("company context is required"),
|
||||
});
|
||||
expect(stateGet).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
await handle.stop().catch(() => undefined);
|
||||
}
|
||||
});
|
||||
|
||||
it("revokes proactive access when the authorized set is cleared", async () => {
|
||||
const { handle, companiesGet } = makeHandle();
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user