From 9f866075d4b472f27453b36d2f1cb4db06ba2379 Mon Sep 17 00:00:00 2001 From: saif <11242541+saifsmailbox98@users.noreply.github.com> Date: Thu, 5 Nov 2026 18:12:02 +0530 Subject: [PATCH 1/4] feat(agent-vault): choose the Agent Vault project with project_id --- infisical_sdk/infisical_requests.py | 25 +++++--- infisical_sdk/resources/agent_vault.py | 72 +++++++++++++++++----- tests/test_agent_vault.py | 85 ++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 25 deletions(-) diff --git a/infisical_sdk/infisical_requests.py b/infisical_sdk/infisical_requests.py index 5d75363..bbb29fb 100644 --- a/infisical_sdk/infisical_requests.py +++ b/infisical_sdk/infisical_requests.py @@ -184,7 +184,8 @@ def post( self, path: str, model: Type[T], - json: Optional[Dict[str, Any]] = None + json: Optional[Dict[str, Any]] = None, + params: Optional[Dict[str, Any]] = None ) -> APIResponse[T]: """Make a POST request with JSON data""" @@ -193,7 +194,7 @@ def post( # Filter out None values json = {k: v for k, v in json.items() if v is not None} - response = self.session.post(self._build_url(path), json=json) + response = self.session.post(self._build_url(path), json=json, params=params) data = self._handle_response(response) parsed_data = model.from_dict(data) if hasattr(model, 'from_dict') else data @@ -209,7 +210,8 @@ def post_once( self, path: str, model: Type[T], - json: Optional[Dict[str, Any]] = None + json: Optional[Dict[str, Any]] = None, + params: Optional[Dict[str, Any]] = None ) -> APIResponse[T]: """Make a POST request that must not run twice, such as one that creates something. @@ -219,7 +221,7 @@ def post_once( # Filter out None values json = {k: v for k, v in json.items() if v is not None} - response = self.session.post(self._build_url(path), json=json) + response = self.session.post(self._build_url(path), json=json, params=params) data = self._handle_response(response) parsed_data = model.from_dict(data) if hasattr(model, 'from_dict') else data @@ -261,7 +263,8 @@ def put( self, path: str, model: Type[T], - json: Optional[Dict[str, Any]] = None + json: Optional[Dict[str, Any]] = None, + params: Optional[Dict[str, Any]] = None ) -> APIResponse[T]: """Make a PUT request with JSON data. A PUT replaces the whole resource, so it's safe to retry.""" @@ -270,7 +273,7 @@ def put( # Filter out None values json = {k: v for k, v in json.items() if v is not None} - response = self.session.put(self._build_url(path), json=json) + response = self.session.put(self._build_url(path), json=json, params=params) data = self._handle_response(response) parsed_data = model.from_dict(data) if hasattr(model, 'from_dict') else data @@ -286,7 +289,8 @@ def delete( self, path: str, model: Type[T], - json: Optional[Dict[str, Any]] = None + json: Optional[Dict[str, Any]] = None, + params: Optional[Dict[str, Any]] = None ) -> APIResponse[T]: """Make a DELETE request with JSON data""" @@ -295,7 +299,7 @@ def delete( # Filter out None values json = {k: v for k, v in json.items() if v is not None} - response = self.session.delete(self._build_url(path), json=json) + response = self.session.delete(self._build_url(path), json=json, params=params) data = self._handle_response(response) parsed_data = model.from_dict(data) if hasattr(model, 'from_dict') else data @@ -311,7 +315,8 @@ def delete_once( self, path: str, model: Type[T], - json: Optional[Dict[str, Any]] = None + json: Optional[Dict[str, Any]] = None, + params: Optional[Dict[str, Any]] = None ) -> APIResponse[T]: """Make a DELETE request that must not run twice. It's retried only in the same cases as post_once.""" @@ -320,7 +325,7 @@ def delete_once( # Filter out None values json = {k: v for k, v in json.items() if v is not None} - response = self.session.delete(self._build_url(path), json=json) + response = self.session.delete(self._build_url(path), json=json, params=params) data = self._handle_response(response) parsed_data = model.from_dict(data) if hasattr(model, 'from_dict') else data diff --git a/infisical_sdk/resources/agent_vault.py b/infisical_sdk/resources/agent_vault.py index 5ebc411..4c9e3d1 100644 --- a/infisical_sdk/resources/agent_vault.py +++ b/infisical_sdk/resources/agent_vault.py @@ -88,8 +88,9 @@ def list( order_by: str = "createdAt", order_direction: str = "desc", limit: int = 20, - offset: int = 0) -> ListAgentVaultProxiesResponse: - """List the organization's Agent Vault proxies. + offset: int = 0, + project_id: Optional[str] = None) -> ListAgentVaultProxiesResponse: + """List an Agent Vault project's proxies. Args: search: Match proxies by name or description. @@ -97,6 +98,7 @@ def list( order_direction: Which way to sort: "asc" or "desc". Defaults to "desc". limit: The maximum number of proxies to return (1-100). Defaults to 20. offset: How many proxies to skip. Defaults to 0. + project_id: The ID of the Agent Vault project to list proxies from. Without it, Infisical uses the organization's original Agent Vault project, which later versions will stop doing. Returns: ListAgentVaultProxiesResponse containing proxies and totalCount. Settings fields are only set for admins. @@ -107,6 +109,7 @@ def list( "orderDirection": order_direction, "limit": limit, "offset": offset, + "projectId": project_id, } result = self.requests.get( @@ -123,7 +126,8 @@ def create( traffic_policy: Union[AgentVaultTrafficPolicy, str] = AgentVaultTrafficPolicy.ANY_HOST, allowed_hosts: Optional[Union[str, List[str]]] = None, poll_interval: int = 60, - description: Optional[str] = None) -> AgentVaultProxyWithTokenResponse: + description: Optional[str] = None, + project_id: Optional[str] = None) -> AgentVaultProxyWithTokenResponse: """Register an Agent Vault proxy and issue its proxy token. Args: @@ -132,6 +136,7 @@ def create( allowed_hosts: Hosts that stay reachable under the "service-hosts" traffic policy even though none of the session's services cover them. A list or a comma-separated string. poll_interval: How often, in seconds, the proxy refreshes its sessions and settings (10-300). Defaults to 60. description: What the proxy is for, up to 256 characters, without line breaks or other control characters. + project_id: The ID of the Agent Vault project to register the proxy in. Without it, Infisical uses the organization's original Agent Vault project, which later versions will stop doing. Returns: AgentVaultProxyWithTokenResponse containing the proxy and its proxyToken. The token is only returned once. @@ -142,6 +147,7 @@ def create( "allowedHosts": _join_hosts(allowed_hosts), "pollInterval": poll_interval, "description": description, + "projectId": project_id, } result = self.requests.post_once( @@ -257,7 +263,8 @@ def list( statuses: Optional[List[Union[AgentVaultSessionStatus, str]]] = None, search: Optional[str] = None, limit: int = 20, - offset: int = 0) -> ListAgentVaultSessionsResponse: + offset: int = 0, + project_id: Optional[str] = None) -> ListAgentVaultSessionsResponse: """List Agent Vault sessions. Args: @@ -266,6 +273,7 @@ def list( search: Match sessions by label, actor name, actor email, the name or email of the user a session was started on behalf of, or access bundle name. limit: The maximum number of sessions to return (1-100). Defaults to 20. offset: How many sessions to skip. Defaults to 0. + project_id: The ID of the Agent Vault project to list sessions from. Without it, Infisical uses the organization's original Agent Vault project, which later versions will stop doing. Returns: ListAgentVaultSessionsResponse containing sessions and totalCount. @@ -280,6 +288,7 @@ def list( "search": search, "limit": limit, "offset": offset, + "projectId": project_id, } result = self.requests.get( @@ -315,7 +324,8 @@ def create( label: Optional[str] = None, on_behalf_of_user_email: Optional[str] = None, on_behalf_of_user_id: Optional[str] = None, - services: Optional[List[Dict[str, Any]]] = None) -> CreatedAgentVaultSession: + services: Optional[List[Dict[str, Any]]] = None, + project_id: Optional[str] = None) -> CreatedAgentVaultSession: """Create an Agent Vault session with an access bundle you can reach, services of its own, or both, for yourself or, as a machine identity with a delegation, on behalf of a user. Only Agent Vault admins can send services with a session. On behalf of a user, that's your machine identity: @@ -333,6 +343,7 @@ def create( on_behalf_of_user_email: The email address of the user to start the session on behalf of. Can't be combined with on_behalf_of_user_id. on_behalf_of_user_id: The ID of the user to start the session on behalf of. Can't be combined with on_behalf_of_user_email. services: Services the session carries itself, instead of or beside its access bundle's: up to 20 dicts with the API's field names, such as {"type": "http", "hostPattern": "api.github.com", "credential": {"type": "bearer", "value": token}}. Only Agent Vault admins can send them. Each has the same fields as a service in an access bundle, except that its name is optional, its values can't use variables, it can't use OAuth or member-provided values, and an MCP server can't be limited to some of its tools. To limit an MCP server's tools, add it to an access bundle instead. + project_id: The ID of the Agent Vault project to create the session in. Without it, Infisical uses the organization's original Agent Vault project, which later versions will stop doing. Returns: The created session, including its token. The token is only returned once. Its services leave out their tokens, passwords and values. @@ -352,6 +363,7 @@ def create( "ttl": ttl, "label": label, "onBehalfOf": on_behalf_of, + "projectId": project_id, } result = self.requests.post_once( @@ -418,7 +430,8 @@ def list( order_by: str = "createdAt", order_direction: str = "desc", limit: int = 20, - offset: int = 0) -> ListAgentVaultAccessBundlesResponse: + offset: int = 0, + project_id: Optional[str] = None) -> ListAgentVaultAccessBundlesResponse: """List the Agent Vault access bundles you can reach. Admins reach every access bundle. Args: @@ -431,6 +444,7 @@ def list( order_direction: Which way to sort: "asc" or "desc". Defaults to "desc". limit: The maximum number of access bundles to return (1-100). Defaults to 20. offset: How many access bundles to skip. Defaults to 0. + project_id: The ID of the Agent Vault project to list access bundles from. Without it, Infisical uses the organization's original Agent Vault project, which later versions will stop doing. Returns: ListAgentVaultAccessBundlesResponse containing accessBundles and totalCount. @@ -447,6 +461,7 @@ def list( "orderDirection": order_direction, "limit": limit, "offset": offset, + "projectId": project_id, } result = self.requests.get( @@ -473,12 +488,17 @@ def get(self, access_bundle_id: str) -> AgentVaultAccessBundle: return result.data.accessBundle - def create(self, name: str, description: Optional[str] = None) -> AgentVaultAccessBundle: + def create( + self, + name: str, + description: Optional[str] = None, + project_id: Optional[str] = None) -> AgentVaultAccessBundle: """Create an Agent Vault access bundle. It's granted to you, unless you're in Agent Vault only through a group. Args: name: The name of the access bundle (a slug, at most 64 characters). description: What the access bundle is for, up to 256 characters, without line breaks or other control characters. + project_id: The ID of the Agent Vault project to create the access bundle in. Without it, Infisical uses the organization's original Agent Vault project, which later versions will stop doing. Returns: The created access bundle. @@ -486,6 +506,7 @@ def create(self, name: str, description: Optional[str] = None) -> AgentVaultAcce request_body = { "name": name, "description": description, + "projectId": project_id, } result = self.requests.post_once( @@ -678,7 +699,9 @@ def revoke( class AgentVaultDelegations: - """Manages Agent Vault delegations, which let machine identities create sessions on behalf of users. Every method requires the Admin role in Agent Vault, except that a machine identity can get its own delegation.""" + """Manages Agent Vault delegations, which let machine identities create sessions on behalf of users. Every method requires the Admin role in Agent Vault, except that a machine identity can get its own delegation. + + A machine identity can be a member of several Agent Vault projects and has a separate delegation in each, so every method takes project_id.""" def __init__(self, requests: InfisicalRequests) -> None: self.requests = requests @@ -687,13 +710,15 @@ def list( self, machine_identity_ids: Optional[List[str]] = None, limit: int = 100, - offset: int = 0) -> ListAgentVaultDelegationsResponse: + offset: int = 0, + project_id: Optional[str] = None) -> ListAgentVaultDelegationsResponse: """List the machine identities that can create Agent Vault sessions on behalf of users. Args: machine_identity_ids: Only return the delegations of these machine identities, up to 100. limit: The maximum number of delegations to return (1-100). Defaults to 100. offset: How many delegations to skip. Defaults to 0. + project_id: The ID of the Agent Vault project to list delegations from. Without it, Infisical uses the organization's original Agent Vault project, which later versions will stop doing. Returns: ListAgentVaultDelegationsResponse containing delegations and totalCount. missingAccessBundles isn't set on these delegations; call get for it. @@ -702,6 +727,7 @@ def list( "machineIdentityIds": ",".join(machine_identity_ids) if machine_identity_ids else None, "limit": limit, "offset": offset, + "projectId": project_id, } result = self.requests.get( @@ -712,17 +738,19 @@ def list( return result.data - def get(self, machine_identity_id: str) -> AgentVaultDelegation: + def get(self, machine_identity_id: str, project_id: Optional[str] = None) -> AgentVaultDelegation: """Get which users a machine identity can create Agent Vault sessions on behalf of. Raises an APIError with status 404 if the machine identity has no delegation. Args: machine_identity_id: The ID of the machine identity. + project_id: The ID of the Agent Vault project the delegation belongs to. Without it, Infisical uses the organization's original Agent Vault project, which later versions will stop doing. Returns: The delegation. missingAccessBundles lists the access bundles that the users it covers can use and the machine identity can't, so the machine identity can't create sessions over them. They're empty when a machine identity that isn't an admin gets its own delegation. """ result = self.requests.get( path=f"/api/v1/agent-vault/delegations/{_segment(machine_identity_id)}", + params={"projectId": project_id}, model=SingleAgentVaultDelegationResponse ) @@ -733,7 +761,8 @@ def set( machine_identity_id: str, scope: Union[AgentVaultDelegationScope, str], user_ids: Optional[List[str]] = None, - group_ids: Optional[List[str]] = None) -> AgentVaultDelegation: + group_ids: Optional[List[str]] = None, + project_id: Optional[str] = None) -> AgentVaultDelegation: """Set which users a machine identity can create Agent Vault sessions on behalf of, replacing any delegation it has. Args: @@ -741,6 +770,7 @@ def set( scope: AgentVaultDelegationScope.ALL_MEMBERS ("all-members") for every Agent Vault member, admins included, or AgentVaultDelegationScope.SELECTED ("selected") for only the users and groups you name. user_ids: With the "selected" scope, the users the machine identity can act on behalf of, up to 100. Each must be a member of Agent Vault. For a user who's in Agent Vault only through a group, name the group instead. group_ids: With the "selected" scope, the groups whose members the machine identity can act on behalf of, up to 100. Each must be a member of Agent Vault. + project_id: The ID of the Agent Vault project the delegation belongs to. Without it, Infisical uses the organization's original Agent Vault project, which later versions will stop doing. Returns: The saved delegation, with missingAccessBundles. @@ -754,16 +784,18 @@ def set( result = self.requests.put( path=f"/api/v1/agent-vault/delegations/{_segment(machine_identity_id)}", json=request_body, + params={"projectId": project_id}, model=SingleAgentVaultDelegationResponse ) return result.data.delegation - def delete(self, machine_identity_id: str) -> AgentVaultDelegation: + def delete(self, machine_identity_id: str, project_id: Optional[str] = None) -> AgentVaultDelegation: """Stop a machine identity from creating Agent Vault sessions on behalf of users. The proxy refuses the sessions it already created on their behalf until a delegation covers those users again, so revoke the sessions to end them for good. Args: machine_identity_id: The ID of the machine identity. + project_id: The ID of the Agent Vault project the delegation belongs to. Without it, Infisical uses the organization's original Agent Vault project, which later versions will stop doing. Returns: The deleted delegation. missingAccessBundles isn't set. @@ -772,6 +804,7 @@ def delete(self, machine_identity_id: str) -> AgentVaultDelegation: # 404 for a delete that worked. result = self.requests.delete_once( path=f"/api/v1/agent-vault/delegations/{_segment(machine_identity_id)}", + params={"projectId": project_id}, model=SingleAgentVaultDelegationResponse ) @@ -781,13 +814,15 @@ def add_subjects( self, machine_identity_id: str, user_ids: Optional[List[str]] = None, - group_ids: Optional[List[str]] = None) -> AgentVaultDelegation: + group_ids: Optional[List[str]] = None, + project_id: Optional[str] = None) -> AgentVaultDelegation: """Add users and groups to a machine identity's delegation, keeping the ones it already names. If the machine identity has no delegation, this creates one with the "selected" scope. A delegation to all members can't take users or groups. Args: machine_identity_id: The ID of the machine identity. user_ids: The users to add, up to 100. Each must be a member of Agent Vault. Users the delegation already names are skipped. group_ids: The groups to add, up to 100. Each must be a member of Agent Vault. Groups the delegation already names are skipped. + project_id: The ID of the Agent Vault project the delegation belongs to. Without it, Infisical uses the organization's original Agent Vault project, which later versions will stop doing. Returns: The delegation after the change, with missingAccessBundles. @@ -800,6 +835,7 @@ def add_subjects( result = self.requests.post( path=f"/api/v1/agent-vault/delegations/{_segment(machine_identity_id)}/subjects", json=request_body, + params={"projectId": project_id}, model=SingleAgentVaultDelegationResponse ) @@ -809,13 +845,15 @@ def remove_subjects( self, machine_identity_id: str, user_ids: Optional[List[str]] = None, - group_ids: Optional[List[str]] = None) -> Optional[AgentVaultDelegation]: + group_ids: Optional[List[str]] = None, + project_id: Optional[str] = None) -> Optional[AgentVaultDelegation]: """Take users and groups out of a machine identity's delegation. The proxy refuses the sessions it created on behalf of the users you take out until the delegation covers them again, so revoke the sessions to end them for good. Taking out the last user or group removes the delegation. Args: machine_identity_id: The ID of the machine identity. user_ids: The users to take out, up to 100. Users the delegation doesn't name are skipped. group_ids: The groups to take out, up to 100. Groups the delegation doesn't name are skipped. + project_id: The ID of the Agent Vault project the delegation belongs to. Without it, Infisical uses the organization's original Agent Vault project, which later versions will stop doing. Returns: The delegation after the change, with missingAccessBundles, or None if it no longer names anyone. @@ -830,6 +868,7 @@ def remove_subjects( result = self.requests.delete_once( path=f"/api/v1/agent-vault/delegations/{_segment(machine_identity_id)}/subjects", json=request_body, + params={"projectId": project_id}, model=SingleAgentVaultDelegationResponse ) @@ -841,7 +880,8 @@ def list_available_subjects( actor_type: Optional[Union[AgentVaultMemberType, str]] = None, search: Optional[str] = None, limit: int = 20, - offset: int = 0) -> ListAgentVaultMembersResponse: + offset: int = 0, + project_id: Optional[str] = None) -> ListAgentVaultMembersResponse: """List the Agent Vault users and groups that can still be added to a machine identity's delegation. Args: @@ -850,6 +890,7 @@ def list_available_subjects( search: Match users by name, username or email address, and groups by name. limit: The maximum number of users and groups to return (1-100). Defaults to 20. offset: How many users and groups to skip. Defaults to 0. + project_id: The ID of the Agent Vault project the delegation belongs to. Without it, Infisical uses the organization's original Agent Vault project, which later versions will stop doing. Returns: ListAgentVaultMembersResponse containing the users and groups in members, and totalCount. @@ -859,6 +900,7 @@ def list_available_subjects( "search": search, "limit": limit, "offset": offset, + "projectId": project_id, } result = self.requests.get( diff --git a/tests/test_agent_vault.py b/tests/test_agent_vault.py index c8bc9f4..355a495 100644 --- a/tests/test_agent_vault.py +++ b/tests/test_agent_vault.py @@ -520,3 +520,88 @@ def test_deletes_are_not_retried_once_the_request_may_have_reached_the_server(ap with pytest.raises(requests.exceptions.ReadTimeout): call(agent_vault) assert api.session.delete.call_count == 1 + + +PROJECT_ID = "4d6f8a1c-2e3b-4c5d-9e7f-1a2b3c4d5e6f" + +LISTS = [ + ("proxies.list", lambda agent_vault, **kwargs: agent_vault.proxies.list(**kwargs), {"proxies": [], "totalCount": 0}), + ("sessions.list", lambda agent_vault, **kwargs: agent_vault.sessions.list(**kwargs), {"sessions": [], "totalCount": 0}), + ("access_bundles.list", lambda agent_vault, **kwargs: agent_vault.access_bundles.list(**kwargs), + {"accessBundles": [], "totalCount": 0}), + ("delegations.list", lambda agent_vault, **kwargs: agent_vault.delegations.list(**kwargs), + {"delegations": [], "totalCount": 0}), + ("delegations.get", lambda agent_vault, **kwargs: agent_vault.delegations.get(MACHINE_IDENTITY_ID, **kwargs), + {"delegation": DELEGATION}), + ("delegations.list_available_subjects", + lambda agent_vault, **kwargs: agent_vault.delegations.list_available_subjects(MACHINE_IDENTITY_ID, **kwargs), + {"members": [], "totalCount": 0}), +] + +CREATES = [ + ("proxies.create", lambda agent_vault, **kwargs: agent_vault.proxies.create(name="edge-proxy", **kwargs), + {"proxy": PROXY, "proxyToken": "token"}), + ("sessions.create", lambda agent_vault, **kwargs: agent_vault.sessions.create(access_bundle="release", **kwargs), + {"session": dict(SESSION, token="token")}), + ("access_bundles.create", lambda agent_vault, **kwargs: agent_vault.access_bundles.create(name="release", **kwargs), + {"accessBundle": BUNDLE}), +] + +DELEGATION_WRITES = [ + ("delegations.set", "put", + lambda agent_vault, **kwargs: agent_vault.delegations.set(MACHINE_IDENTITY_ID, "all-members", **kwargs)), + ("delegations.delete", "delete", + lambda agent_vault, **kwargs: agent_vault.delegations.delete(MACHINE_IDENTITY_ID, **kwargs)), + ("delegations.add_subjects", "post", + lambda agent_vault, **kwargs: agent_vault.delegations.add_subjects(MACHINE_IDENTITY_ID, user_ids=[ON_BEHALF_OF["id"]], **kwargs)), + ("delegations.remove_subjects", "delete", + lambda agent_vault, **kwargs: agent_vault.delegations.remove_subjects(MACHINE_IDENTITY_ID, user_ids=[ON_BEHALF_OF["id"]], **kwargs)), +] + + +@pytest.mark.parametrize("call, payload", [(call, payload) for _, call, payload in LISTS], ids=[name for name, _, _ in LISTS]) +def test_lists_send_the_project_id_in_the_query(api, agent_vault, json_response, call, payload): + api.session.get.return_value = json_response(payload) + call(agent_vault, project_id=PROJECT_ID) + assert api.session.get.call_args.kwargs["params"]["projectId"] == PROJECT_ID + + +@pytest.mark.parametrize("call, payload", [(call, payload) for _, call, payload in LISTS], ids=[name for name, _, _ in LISTS]) +def test_lists_leave_out_the_project_id_so_the_server_picks_the_project(api, agent_vault, json_response, call, payload): + api.session.get.return_value = json_response(payload) + call(agent_vault) + # requests drops query parameters whose value is None. + assert api.session.get.call_args.kwargs["params"].get("projectId") is None + + +@pytest.mark.parametrize("call, payload", [(call, payload) for _, call, payload in CREATES], ids=[name for name, _, _ in CREATES]) +def test_creates_send_the_project_id_in_the_body(api, agent_vault, json_response, call, payload): + api.session.post.return_value = json_response(payload) + call(agent_vault, project_id=PROJECT_ID) + assert api.session.post.call_args.kwargs["json"]["projectId"] == PROJECT_ID + assert api.session.post.call_args.kwargs.get("params") is None + + +@pytest.mark.parametrize("call, payload", [(call, payload) for _, call, payload in CREATES], ids=[name for name, _, _ in CREATES]) +def test_creates_leave_out_the_project_id_so_the_server_picks_the_project(api, agent_vault, json_response, call, payload): + api.session.post.return_value = json_response(payload) + call(agent_vault) + assert "projectId" not in api.session.post.call_args.kwargs["json"] + + +@pytest.mark.parametrize("verb, call", [(verb, call) for _, verb, call in DELEGATION_WRITES], + ids=[name for name, _, _ in DELEGATION_WRITES]) +def test_delegation_writes_send_the_project_id_in_the_query_and_not_the_body(api, agent_vault, json_response, verb, call): + # A delegation is addressed by its machine identity, which can be in several projects, and the server refuses a + # projectId in the body of these routes. + getattr(api.session, verb).return_value = json_response({"delegation": DELEGATION}) + call(agent_vault, project_id=PROJECT_ID) + sent = getattr(api.session, verb).call_args.kwargs + assert sent["params"] == {"projectId": PROJECT_ID} + assert "projectId" not in (sent["json"] or {}) + + +def test_a_project_id_left_out_of_the_query_is_not_sent(): + prepared = requests.Request("PUT", "https://infisical.example.com/api/v1/agent-vault/delegations/id", + params={"projectId": None}).prepare() + assert prepared.url == "https://infisical.example.com/api/v1/agent-vault/delegations/id" From ae17527c9b9af880ac4ddcc8b223b1d62a1bef6a Mon Sep 17 00:00:00 2001 From: saif <11242541+saifsmailbox98@users.noreply.github.com> Date: Thu, 5 Nov 2026 18:15:58 +0530 Subject: [PATCH 2/4] docs(agent-vault): describe project_id without the fallback --- infisical_sdk/resources/agent_vault.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/infisical_sdk/resources/agent_vault.py b/infisical_sdk/resources/agent_vault.py index 4c9e3d1..2bfaefc 100644 --- a/infisical_sdk/resources/agent_vault.py +++ b/infisical_sdk/resources/agent_vault.py @@ -98,7 +98,7 @@ def list( order_direction: Which way to sort: "asc" or "desc". Defaults to "desc". limit: The maximum number of proxies to return (1-100). Defaults to 20. offset: How many proxies to skip. Defaults to 0. - project_id: The ID of the Agent Vault project to list proxies from. Without it, Infisical uses the organization's original Agent Vault project, which later versions will stop doing. + project_id: The ID of the Agent Vault project to list proxies from. Returns: ListAgentVaultProxiesResponse containing proxies and totalCount. Settings fields are only set for admins. @@ -136,7 +136,7 @@ def create( allowed_hosts: Hosts that stay reachable under the "service-hosts" traffic policy even though none of the session's services cover them. A list or a comma-separated string. poll_interval: How often, in seconds, the proxy refreshes its sessions and settings (10-300). Defaults to 60. description: What the proxy is for, up to 256 characters, without line breaks or other control characters. - project_id: The ID of the Agent Vault project to register the proxy in. Without it, Infisical uses the organization's original Agent Vault project, which later versions will stop doing. + project_id: The ID of the Agent Vault project to register the proxy in. Returns: AgentVaultProxyWithTokenResponse containing the proxy and its proxyToken. The token is only returned once. @@ -273,7 +273,7 @@ def list( search: Match sessions by label, actor name, actor email, the name or email of the user a session was started on behalf of, or access bundle name. limit: The maximum number of sessions to return (1-100). Defaults to 20. offset: How many sessions to skip. Defaults to 0. - project_id: The ID of the Agent Vault project to list sessions from. Without it, Infisical uses the organization's original Agent Vault project, which later versions will stop doing. + project_id: The ID of the Agent Vault project to list sessions from. Returns: ListAgentVaultSessionsResponse containing sessions and totalCount. @@ -343,7 +343,7 @@ def create( on_behalf_of_user_email: The email address of the user to start the session on behalf of. Can't be combined with on_behalf_of_user_id. on_behalf_of_user_id: The ID of the user to start the session on behalf of. Can't be combined with on_behalf_of_user_email. services: Services the session carries itself, instead of or beside its access bundle's: up to 20 dicts with the API's field names, such as {"type": "http", "hostPattern": "api.github.com", "credential": {"type": "bearer", "value": token}}. Only Agent Vault admins can send them. Each has the same fields as a service in an access bundle, except that its name is optional, its values can't use variables, it can't use OAuth or member-provided values, and an MCP server can't be limited to some of its tools. To limit an MCP server's tools, add it to an access bundle instead. - project_id: The ID of the Agent Vault project to create the session in. Without it, Infisical uses the organization's original Agent Vault project, which later versions will stop doing. + project_id: The ID of the Agent Vault project to create the session in. Returns: The created session, including its token. The token is only returned once. Its services leave out their tokens, passwords and values. @@ -444,7 +444,7 @@ def list( order_direction: Which way to sort: "asc" or "desc". Defaults to "desc". limit: The maximum number of access bundles to return (1-100). Defaults to 20. offset: How many access bundles to skip. Defaults to 0. - project_id: The ID of the Agent Vault project to list access bundles from. Without it, Infisical uses the organization's original Agent Vault project, which later versions will stop doing. + project_id: The ID of the Agent Vault project to list access bundles from. Returns: ListAgentVaultAccessBundlesResponse containing accessBundles and totalCount. @@ -498,7 +498,7 @@ def create( Args: name: The name of the access bundle (a slug, at most 64 characters). description: What the access bundle is for, up to 256 characters, without line breaks or other control characters. - project_id: The ID of the Agent Vault project to create the access bundle in. Without it, Infisical uses the organization's original Agent Vault project, which later versions will stop doing. + project_id: The ID of the Agent Vault project to create the access bundle in. Returns: The created access bundle. @@ -718,7 +718,7 @@ def list( machine_identity_ids: Only return the delegations of these machine identities, up to 100. limit: The maximum number of delegations to return (1-100). Defaults to 100. offset: How many delegations to skip. Defaults to 0. - project_id: The ID of the Agent Vault project to list delegations from. Without it, Infisical uses the organization's original Agent Vault project, which later versions will stop doing. + project_id: The ID of the Agent Vault project to list delegations from. Returns: ListAgentVaultDelegationsResponse containing delegations and totalCount. missingAccessBundles isn't set on these delegations; call get for it. @@ -743,7 +743,7 @@ def get(self, machine_identity_id: str, project_id: Optional[str] = None) -> Age Args: machine_identity_id: The ID of the machine identity. - project_id: The ID of the Agent Vault project the delegation belongs to. Without it, Infisical uses the organization's original Agent Vault project, which later versions will stop doing. + project_id: The ID of the Agent Vault project the delegation belongs to. Returns: The delegation. missingAccessBundles lists the access bundles that the users it covers can use and the machine identity can't, so the machine identity can't create sessions over them. They're empty when a machine identity that isn't an admin gets its own delegation. @@ -770,7 +770,7 @@ def set( scope: AgentVaultDelegationScope.ALL_MEMBERS ("all-members") for every Agent Vault member, admins included, or AgentVaultDelegationScope.SELECTED ("selected") for only the users and groups you name. user_ids: With the "selected" scope, the users the machine identity can act on behalf of, up to 100. Each must be a member of Agent Vault. For a user who's in Agent Vault only through a group, name the group instead. group_ids: With the "selected" scope, the groups whose members the machine identity can act on behalf of, up to 100. Each must be a member of Agent Vault. - project_id: The ID of the Agent Vault project the delegation belongs to. Without it, Infisical uses the organization's original Agent Vault project, which later versions will stop doing. + project_id: The ID of the Agent Vault project the delegation belongs to. Returns: The saved delegation, with missingAccessBundles. @@ -795,7 +795,7 @@ def delete(self, machine_identity_id: str, project_id: Optional[str] = None) -> Args: machine_identity_id: The ID of the machine identity. - project_id: The ID of the Agent Vault project the delegation belongs to. Without it, Infisical uses the organization's original Agent Vault project, which later versions will stop doing. + project_id: The ID of the Agent Vault project the delegation belongs to. Returns: The deleted delegation. missingAccessBundles isn't set. @@ -822,7 +822,7 @@ def add_subjects( machine_identity_id: The ID of the machine identity. user_ids: The users to add, up to 100. Each must be a member of Agent Vault. Users the delegation already names are skipped. group_ids: The groups to add, up to 100. Each must be a member of Agent Vault. Groups the delegation already names are skipped. - project_id: The ID of the Agent Vault project the delegation belongs to. Without it, Infisical uses the organization's original Agent Vault project, which later versions will stop doing. + project_id: The ID of the Agent Vault project the delegation belongs to. Returns: The delegation after the change, with missingAccessBundles. @@ -853,7 +853,7 @@ def remove_subjects( machine_identity_id: The ID of the machine identity. user_ids: The users to take out, up to 100. Users the delegation doesn't name are skipped. group_ids: The groups to take out, up to 100. Groups the delegation doesn't name are skipped. - project_id: The ID of the Agent Vault project the delegation belongs to. Without it, Infisical uses the organization's original Agent Vault project, which later versions will stop doing. + project_id: The ID of the Agent Vault project the delegation belongs to. Returns: The delegation after the change, with missingAccessBundles, or None if it no longer names anyone. @@ -890,7 +890,7 @@ def list_available_subjects( search: Match users by name, username or email address, and groups by name. limit: The maximum number of users and groups to return (1-100). Defaults to 20. offset: How many users and groups to skip. Defaults to 0. - project_id: The ID of the Agent Vault project the delegation belongs to. Without it, Infisical uses the organization's original Agent Vault project, which later versions will stop doing. + project_id: The ID of the Agent Vault project the delegation belongs to. Returns: ListAgentVaultMembersResponse containing the users and groups in members, and totalCount. From bd9284b7e5ccba2c9b6d699896e56b9eb6f11632 Mon Sep 17 00:00:00 2001 From: saif <11242541+saifsmailbox98@users.noreply.github.com> Date: Thu, 5 Nov 2026 18:41:37 +0530 Subject: [PATCH 3/4] feat(agent-vault)!: require the Agent Vault project --- infisical_sdk/resources/agent_vault.py | 37 +++++--- tests/test_agent_vault.py | 117 ++++++++++++------------- 2 files changed, 80 insertions(+), 74 deletions(-) diff --git a/infisical_sdk/resources/agent_vault.py b/infisical_sdk/resources/agent_vault.py index 2bfaefc..8069ad8 100644 --- a/infisical_sdk/resources/agent_vault.py +++ b/infisical_sdk/resources/agent_vault.py @@ -89,7 +89,8 @@ def list( order_direction: str = "desc", limit: int = 20, offset: int = 0, - project_id: Optional[str] = None) -> ListAgentVaultProxiesResponse: + *, + project_id: str) -> ListAgentVaultProxiesResponse: """List an Agent Vault project's proxies. Args: @@ -127,7 +128,8 @@ def create( allowed_hosts: Optional[Union[str, List[str]]] = None, poll_interval: int = 60, description: Optional[str] = None, - project_id: Optional[str] = None) -> AgentVaultProxyWithTokenResponse: + *, + project_id: str) -> AgentVaultProxyWithTokenResponse: """Register an Agent Vault proxy and issue its proxy token. Args: @@ -264,7 +266,8 @@ def list( search: Optional[str] = None, limit: int = 20, offset: int = 0, - project_id: Optional[str] = None) -> ListAgentVaultSessionsResponse: + *, + project_id: str) -> ListAgentVaultSessionsResponse: """List Agent Vault sessions. Args: @@ -325,7 +328,8 @@ def create( on_behalf_of_user_email: Optional[str] = None, on_behalf_of_user_id: Optional[str] = None, services: Optional[List[Dict[str, Any]]] = None, - project_id: Optional[str] = None) -> CreatedAgentVaultSession: + *, + project_id: str) -> CreatedAgentVaultSession: """Create an Agent Vault session with an access bundle you can reach, services of its own, or both, for yourself or, as a machine identity with a delegation, on behalf of a user. Only Agent Vault admins can send services with a session. On behalf of a user, that's your machine identity: @@ -431,7 +435,8 @@ def list( order_direction: str = "desc", limit: int = 20, offset: int = 0, - project_id: Optional[str] = None) -> ListAgentVaultAccessBundlesResponse: + *, + project_id: str) -> ListAgentVaultAccessBundlesResponse: """List the Agent Vault access bundles you can reach. Admins reach every access bundle. Args: @@ -492,7 +497,8 @@ def create( self, name: str, description: Optional[str] = None, - project_id: Optional[str] = None) -> AgentVaultAccessBundle: + *, + project_id: str) -> AgentVaultAccessBundle: """Create an Agent Vault access bundle. It's granted to you, unless you're in Agent Vault only through a group. Args: @@ -711,7 +717,8 @@ def list( machine_identity_ids: Optional[List[str]] = None, limit: int = 100, offset: int = 0, - project_id: Optional[str] = None) -> ListAgentVaultDelegationsResponse: + *, + project_id: str) -> ListAgentVaultDelegationsResponse: """List the machine identities that can create Agent Vault sessions on behalf of users. Args: @@ -738,7 +745,7 @@ def list( return result.data - def get(self, machine_identity_id: str, project_id: Optional[str] = None) -> AgentVaultDelegation: + def get(self, machine_identity_id: str, *, project_id: str) -> AgentVaultDelegation: """Get which users a machine identity can create Agent Vault sessions on behalf of. Raises an APIError with status 404 if the machine identity has no delegation. Args: @@ -762,7 +769,8 @@ def set( scope: Union[AgentVaultDelegationScope, str], user_ids: Optional[List[str]] = None, group_ids: Optional[List[str]] = None, - project_id: Optional[str] = None) -> AgentVaultDelegation: + *, + project_id: str) -> AgentVaultDelegation: """Set which users a machine identity can create Agent Vault sessions on behalf of, replacing any delegation it has. Args: @@ -790,7 +798,7 @@ def set( return result.data.delegation - def delete(self, machine_identity_id: str, project_id: Optional[str] = None) -> AgentVaultDelegation: + def delete(self, machine_identity_id: str, *, project_id: str) -> AgentVaultDelegation: """Stop a machine identity from creating Agent Vault sessions on behalf of users. The proxy refuses the sessions it already created on their behalf until a delegation covers those users again, so revoke the sessions to end them for good. Args: @@ -815,7 +823,8 @@ def add_subjects( machine_identity_id: str, user_ids: Optional[List[str]] = None, group_ids: Optional[List[str]] = None, - project_id: Optional[str] = None) -> AgentVaultDelegation: + *, + project_id: str) -> AgentVaultDelegation: """Add users and groups to a machine identity's delegation, keeping the ones it already names. If the machine identity has no delegation, this creates one with the "selected" scope. A delegation to all members can't take users or groups. Args: @@ -846,7 +855,8 @@ def remove_subjects( machine_identity_id: str, user_ids: Optional[List[str]] = None, group_ids: Optional[List[str]] = None, - project_id: Optional[str] = None) -> Optional[AgentVaultDelegation]: + *, + project_id: str) -> Optional[AgentVaultDelegation]: """Take users and groups out of a machine identity's delegation. The proxy refuses the sessions it created on behalf of the users you take out until the delegation covers them again, so revoke the sessions to end them for good. Taking out the last user or group removes the delegation. Args: @@ -881,7 +891,8 @@ def list_available_subjects( search: Optional[str] = None, limit: int = 20, offset: int = 0, - project_id: Optional[str] = None) -> ListAgentVaultMembersResponse: + *, + project_id: str) -> ListAgentVaultMembersResponse: """List the Agent Vault users and groups that can still be added to a machine identity's delegation. Args: diff --git a/tests/test_agent_vault.py b/tests/test_agent_vault.py index 355a495..3c75228 100644 --- a/tests/test_agent_vault.py +++ b/tests/test_agent_vault.py @@ -56,6 +56,7 @@ "customHeaders": [{"name": "X-Team", "prefix": ""}], "substitutions": [], } +PROJECT_ID = "4d6f8a1c-2e3b-4c5d-9e7f-1a2b3c4d5e6f" MACHINE_IDENTITY_ID = "9c4b2a1d-3e5f-4a6b-8c7d-2e1f0a9b8c7d" GROUP_ID = "7b1e4f2a-9c3d-4a6b-8e5f-1d2c3b4a5e6f" SERVICE_ID = "3a5c7e9b-2d4f-4a6c-8e1b-5d7f9a2c4e6b" @@ -93,7 +94,7 @@ def test_proxies_update_leaves_out_values_you_do_not_pass(api, agent_vault, json def test_proxies_create_sends_and_reads_the_description(api, agent_vault, json_response): api.session.post.return_value = json_response({"proxy": dict(PROXY, description="Edge traffic"), "proxyToken": "token"}) - created = agent_vault.proxies.create(name="edge-proxy", description="Edge traffic") + created = agent_vault.proxies.create(name="edge-proxy", description="Edge traffic", project_id=PROJECT_ID) assert api.session.post.call_args.kwargs["json"]["description"] == "Edge traffic" assert created.proxy.description == "Edge traffic" @@ -116,9 +117,9 @@ def test_sessions_update_always_sends_the_label(api, agent_vault, json_response, @pytest.mark.parametrize("create", [ - lambda agent_vault: agent_vault.sessions.create(access_bundle="release"), - lambda agent_vault: agent_vault.proxies.create(name="edge-proxy"), - lambda agent_vault: agent_vault.access_bundles.create(name="release"), + lambda agent_vault: agent_vault.sessions.create(access_bundle="release", project_id=PROJECT_ID), + lambda agent_vault: agent_vault.proxies.create(name="edge-proxy", project_id=PROJECT_ID), + lambda agent_vault: agent_vault.access_bundles.create(name="release", project_id=PROJECT_ID), ], ids=["sessions.create", "proxies.create", "access_bundles.create"]) def test_creates_are_not_retried_once_the_request_may_have_reached_the_server(api, agent_vault, create): api.session.post.side_effect = requests.exceptions.ReadTimeout() @@ -130,7 +131,7 @@ def test_creates_are_not_retried_once_the_request_may_have_reached_the_server(ap @pytest.mark.parametrize("allowed_hosts", [[], ""], ids=["empty list", "empty string"]) def test_proxies_create_leaves_out_empty_allowed_hosts(api, agent_vault, json_response, allowed_hosts): api.session.post.return_value = json_response({"proxy": PROXY, "proxyToken": "token"}) - agent_vault.proxies.create(name="edge-proxy", allowed_hosts=allowed_hosts) + agent_vault.proxies.create(name="edge-proxy", allowed_hosts=allowed_hosts, project_id=PROJECT_ID) assert "allowedHosts" not in api.session.post.call_args.kwargs["json"] @@ -163,15 +164,15 @@ def test_sessions_get_keeps_the_name_of_a_deleted_access_bundle(api, agent_vault def test_sessions_create_sends_one_access_bundle(api, agent_vault, json_response): created = {"id": SESSION["id"], "token": "agv_token", "createdAt": SESSION["createdAt"], "accessBundle": SESSION["accessBundle"]} api.session.post.return_value = json_response({"session": created}) - session = agent_vault.sessions.create(access_bundle="release", ttl="1h") - assert api.session.post.call_args.kwargs["json"] == {"accessBundle": "release", "ttl": "1h"} + session = agent_vault.sessions.create(access_bundle="release", ttl="1h", project_id=PROJECT_ID) + assert api.session.post.call_args.kwargs["json"] == {"accessBundle": "release", "ttl": "1h", "projectId": PROJECT_ID} assert session.accessBundle.name == "release" def test_sessions_create_leaves_out_ttl_so_the_server_picks_the_default(api, agent_vault, json_response): api.session.post.return_value = json_response({"session": dict(SESSION, token="token")}) - agent_vault.sessions.create(access_bundle="release") - assert api.session.post.call_args.kwargs["json"] == {"accessBundle": "release"} + agent_vault.sessions.create(access_bundle="release", project_id=PROJECT_ID) + assert api.session.post.call_args.kwargs["json"] == {"accessBundle": "release", "projectId": PROJECT_ID} @pytest.mark.parametrize("kwargs, on_behalf_of", [ @@ -181,14 +182,14 @@ def test_sessions_create_leaves_out_ttl_so_the_server_picks_the_default(api, age ], ids=["email", "id", "empty email"]) def test_sessions_create_names_the_user_it_acts_for(api, agent_vault, json_response, kwargs, on_behalf_of): api.session.post.return_value = json_response({"session": dict(SESSION, token="token")}) - agent_vault.sessions.create(access_bundle="release", **kwargs) - assert api.session.post.call_args.kwargs["json"] == {"accessBundle": "release", "onBehalfOf": on_behalf_of} + agent_vault.sessions.create(access_bundle="release", project_id=PROJECT_ID, **kwargs) + assert api.session.post.call_args.kwargs["json"] == {"accessBundle": "release", "onBehalfOf": on_behalf_of, "projectId": PROJECT_ID} def test_sessions_create_still_takes_the_access_bundle_first(api, agent_vault, json_response): api.session.post.return_value = json_response({"session": dict(SESSION, token="token")}) - agent_vault.sessions.create("release", "1h") - assert api.session.post.call_args.kwargs["json"] == {"accessBundle": "release", "ttl": "1h"} + agent_vault.sessions.create("release", "1h", project_id=PROJECT_ID) + assert api.session.post.call_args.kwargs["json"] == {"accessBundle": "release", "ttl": "1h", "projectId": PROJECT_ID} def test_sessions_create_rejects_both_a_user_email_and_a_user_id(api, agent_vault): @@ -197,6 +198,7 @@ def test_sessions_create_rejects_both_a_user_email_and_a_user_id(api, agent_vaul access_bundle="release", on_behalf_of_user_email=ON_BEHALF_OF["email"], on_behalf_of_user_id=ON_BEHALF_OF["id"], + project_id=PROJECT_ID, ) api.session.post.assert_not_called() @@ -211,21 +213,21 @@ def test_sessions_get_reads_the_user_a_session_acts_for(api, agent_vault, json_r def test_access_bundles_list_sends_booleans_the_way_the_api_reads_them(api, agent_vault, json_response): api.session.get.return_value = json_response({"accessBundles": [], "totalCount": 0}) - agent_vault.access_bundles.list(requires_setup=True, granted=False) + agent_vault.access_bundles.list(requires_setup=True, granted=False, project_id=PROJECT_ID) params = api.session.get.call_args.kwargs["params"] assert (params["requiresSetup"], params["granted"]) == ("true", "false") def test_access_bundles_list_names_the_user_a_session_would_act_for(api, agent_vault, json_response): api.session.get.return_value = json_response({"accessBundles": [], "totalCount": 0}) - agent_vault.access_bundles.list(on_behalf_of_user_email=ON_BEHALF_OF["email"]) + agent_vault.access_bundles.list(on_behalf_of_user_email=ON_BEHALF_OF["email"], project_id=PROJECT_ID) params = api.session.get.call_args.kwargs["params"] assert (params["onBehalfOfUserEmail"], params["onBehalfOfUserId"]) == (ON_BEHALF_OF["email"], None) def test_access_bundles_list_rejects_both_a_user_email_and_a_user_id(api, agent_vault): with pytest.raises(ValueError, match="not both"): - agent_vault.access_bundles.list(on_behalf_of_user_email=ON_BEHALF_OF["email"], on_behalf_of_user_id=ON_BEHALF_OF["id"]) + agent_vault.access_bundles.list(on_behalf_of_user_email=ON_BEHALF_OF["email"], on_behalf_of_user_id=ON_BEHALF_OF["id"], project_id=PROJECT_ID) api.session.get.assert_not_called() @@ -233,7 +235,7 @@ def test_access_bundles_list_reads_the_services_requiring_setup(api, agent_vault listed = dict(BUNDLE, serviceCount=1, memberCount=2, hostPatterns=["api.github.com"], servicesRequiringSetup=[{"id": SERVICE_ID, "name": "github"}]) api.session.get.return_value = json_response({"accessBundles": [listed], "totalCount": 1}) - bundle = agent_vault.access_bundles.list().accessBundles[0] + bundle = agent_vault.access_bundles.list(project_id=PROJECT_ID).accessBundles[0] assert [(service.id, service.name) for service in bundle.servicesRequiringSetup] == [(SERVICE_ID, "github")] assert bundle.services is None @@ -269,13 +271,13 @@ def test_access_bundles_grant_and_revoke_send_only_the_grantees_you_name(api, ag def test_delegations_set_puts_the_scope_value(api, agent_vault, json_response): api.session.put.return_value = json_response({"delegation": DELEGATION}) - agent_vault.delegations.set(MACHINE_IDENTITY_ID, AgentVaultDelegationScope.SELECTED, group_ids=[GROUP_ID]) + agent_vault.delegations.set(MACHINE_IDENTITY_ID, AgentVaultDelegationScope.SELECTED, group_ids=[GROUP_ID], project_id=PROJECT_ID) assert api.session.put.call_args.kwargs["json"] == {"scope": "selected", "groupIds": [GROUP_ID]} def test_delegations_get_reads_users_groups_and_missing_access_bundles(api, agent_vault, json_response): api.session.get.return_value = json_response({"delegation": DELEGATION}) - delegation = agent_vault.delegations.get(MACHINE_IDENTITY_ID) + delegation = agent_vault.delegations.get(MACHINE_IDENTITY_ID, project_id=PROJECT_ID) assert delegation.machineIdentity.name == "slack-bot" assert [bundle.name for bundle in delegation.users[0].missingAccessBundles] == ["release"] assert (delegation.groups[0].name, delegation.groups[0].missingAccessBundles) == ("Engineering", []) @@ -285,7 +287,7 @@ def test_delegations_list_joins_the_machine_identity_ids(api, agent_vault, json_ other_machine_identity_id = "2f6e8a4c-1b3d-4c5e-9f7a-6b8c0d2e4f1a" listed = {key: value for key, value in DELEGATION.items() if key != "missingAccessBundles"} api.session.get.return_value = json_response({"delegations": [listed], "totalCount": 1}) - delegations = agent_vault.delegations.list(machine_identity_ids=[MACHINE_IDENTITY_ID, other_machine_identity_id]) + delegations = agent_vault.delegations.list(machine_identity_ids=[MACHINE_IDENTITY_ID, other_machine_identity_id], project_id=PROJECT_ID) expected = f"{MACHINE_IDENTITY_ID},{other_machine_identity_id}" assert api.session.get.call_args.kwargs["params"]["machineIdentityIds"] == expected assert delegations.delegations[0].missingAccessBundles is None @@ -293,13 +295,13 @@ def test_delegations_list_joins_the_machine_identity_ids(api, agent_vault, json_ def test_delegations_remove_subjects_returns_none_once_the_last_one_is_gone(api, agent_vault, json_response): api.session.delete.return_value = json_response({"delegation": None}) - assert agent_vault.delegations.remove_subjects(MACHINE_IDENTITY_ID, user_ids=[ON_BEHALF_OF["id"]]) is None + assert agent_vault.delegations.remove_subjects(MACHINE_IDENTITY_ID, user_ids=[ON_BEHALF_OF["id"]], project_id=PROJECT_ID) is None assert api.session.delete.call_args.kwargs["json"] == {"userIds": [ON_BEHALF_OF["id"]]} def test_delegations_list_available_subjects_sends_the_actor_type_value(api, agent_vault, json_response): api.session.get.return_value = json_response({"members": [], "totalCount": 0}) - agent_vault.delegations.list_available_subjects(MACHINE_IDENTITY_ID, actor_type=AgentVaultMemberType.GROUP) + agent_vault.delegations.list_available_subjects(MACHINE_IDENTITY_ID, actor_type=AgentVaultMemberType.GROUP, project_id=PROJECT_ID) assert api.session.get.call_args.kwargs["params"]["actorType"] == "group" @@ -310,7 +312,7 @@ def test_proxies_send_either_name_of_service_hosts_as_bundle_hosts_which_every_v api, agent_vault, json_response, traffic_policy): api.session.post.return_value = json_response({"proxy": PROXY, "proxyToken": "token"}) api.session.patch.return_value = json_response({"proxy": PROXY}) - agent_vault.proxies.create(name="edge-proxy", traffic_policy=traffic_policy) + agent_vault.proxies.create(name="edge-proxy", traffic_policy=traffic_policy, project_id=PROJECT_ID) agent_vault.proxies.update(PROXY["id"], traffic_policy=traffic_policy) assert api.session.post.call_args.kwargs["json"]["trafficPolicy"] == "bundle-hosts" assert api.session.patch.call_args.kwargs["json"] == {"trafficPolicy": "bundle-hosts"} @@ -320,15 +322,15 @@ def test_proxies_send_either_name_of_service_hosts_as_bundle_hosts_which_every_v ids=["default", "ANY_HOST", "any-host"]) def test_proxies_create_sends_any_host_as_it_is(api, agent_vault, json_response, kwargs): api.session.post.return_value = json_response({"proxy": PROXY, "proxyToken": "token"}) - agent_vault.proxies.create(name="edge-proxy", **kwargs) + agent_vault.proxies.create(name="edge-proxy", project_id=PROJECT_ID, **kwargs) assert api.session.post.call_args.kwargs["json"]["trafficPolicy"] == "any-host" @pytest.mark.parametrize("reported", ["service-hosts", "bundle-hosts"], ids=["current version", "version before the rename"]) @pytest.mark.parametrize("verb, payload, call", [ - ("get", lambda proxy: {"proxies": [proxy], "totalCount": 1}, lambda agent_vault: agent_vault.proxies.list().proxies[0]), + ("get", lambda proxy: {"proxies": [proxy], "totalCount": 1}, lambda agent_vault: agent_vault.proxies.list(project_id=PROJECT_ID).proxies[0]), ("post", lambda proxy: {"proxy": proxy, "proxyToken": "token"}, - lambda agent_vault: agent_vault.proxies.create(name="edge-proxy").proxy), + lambda agent_vault: agent_vault.proxies.create(name="edge-proxy", project_id=PROJECT_ID).proxy), ("patch", lambda proxy: {"proxy": proxy}, lambda agent_vault: agent_vault.proxies.update(PROXY["id"], poll_interval=30)), ("post", lambda proxy: {"proxy": proxy, "proxyToken": "token"}, lambda agent_vault: agent_vault.proxies.rotate_token(PROXY["id"]).proxy), @@ -344,7 +346,7 @@ def test_proxies_report_either_name_as_service_hosts(api, agent_vault, json_resp def test_proxies_list_leaves_the_traffic_policy_unset_for_a_member(api, agent_vault, json_response): api.session.get.return_value = json_response({"proxies": [PROXY], "totalCount": 1}) - assert agent_vault.proxies.list().proxies[0].trafficPolicy is None + assert agent_vault.proxies.list(project_id=PROJECT_ID).proxies[0].trafficPolicy is None def test_traffic_policy_old_name_is_the_same_member(): @@ -358,20 +360,20 @@ def test_traffic_policy_old_name_is_the_same_member(): def test_sessions_create_sends_services_without_an_access_bundle(api, agent_vault, json_response): api.session.post.return_value = json_response({"session": dict(SESSION, token="token", services=[INLINE_SERVICE])}) - agent_vault.sessions.create(services=[INLINE_SERVICE_INPUT]) - assert api.session.post.call_args.kwargs["json"] == {"services": [INLINE_SERVICE_INPUT]} + agent_vault.sessions.create(services=[INLINE_SERVICE_INPUT], project_id=PROJECT_ID) + assert api.session.post.call_args.kwargs["json"] == {"services": [INLINE_SERVICE_INPUT], "projectId": PROJECT_ID} def test_sessions_create_sends_an_access_bundle_and_services_together(api, agent_vault, json_response): api.session.post.return_value = json_response({"session": dict(SESSION, token="token", services=[INLINE_SERVICE])}) - agent_vault.sessions.create(access_bundle="release", services=[INLINE_SERVICE_INPUT]) - assert api.session.post.call_args.kwargs["json"] == {"accessBundle": "release", "services": [INLINE_SERVICE_INPUT]} + agent_vault.sessions.create(access_bundle="release", services=[INLINE_SERVICE_INPUT], project_id=PROJECT_ID) + assert api.session.post.call_args.kwargs["json"] == {"accessBundle": "release", "services": [INLINE_SERVICE_INPUT], "projectId": PROJECT_ID} def test_sessions_create_sends_an_empty_access_bundle_name_so_the_server_rejects_it(api, agent_vault, json_response): api.session.post.return_value = json_response({"session": dict(SESSION, token="token")}) - agent_vault.sessions.create(access_bundle="") - assert api.session.post.call_args.kwargs["json"] == {"accessBundle": ""} + agent_vault.sessions.create(access_bundle="", project_id=PROJECT_ID) + assert api.session.post.call_args.kwargs["json"] == {"accessBundle": "", "projectId": PROJECT_ID} def test_sessions_read_services_as_the_api_sends_them(api, agent_vault, json_response): @@ -382,9 +384,9 @@ def test_sessions_read_services_as_the_api_sends_them(api, agent_vault, json_res json_response({"sessions": [session], "totalCount": 1}), ] expected = [AgentVaultInlineService(**INLINE_SERVICE)] - assert agent_vault.sessions.create(services=[INLINE_SERVICE_INPUT]).services == expected + assert agent_vault.sessions.create(services=[INLINE_SERVICE_INPUT], project_id=PROJECT_ID).services == expected assert agent_vault.sessions.get(SESSION["id"]).services == expected - assert agent_vault.sessions.list().sessions[0].services == expected + assert agent_vault.sessions.list(project_id=PROJECT_ID).sessions[0].services == expected def test_sessions_from_an_infisical_version_without_inline_services_have_an_empty_list_of_them( @@ -396,10 +398,10 @@ def test_sessions_from_an_infisical_version_without_inline_services_have_an_empt json_response({"session": older}), json_response({"sessions": [older], "totalCount": 1}), ] - assert agent_vault.sessions.create(access_bundle="release").services == [] + assert agent_vault.sessions.create(access_bundle="release", project_id=PROJECT_ID).services == [] assert agent_vault.sessions.update(SESSION["id"], None).services == [] assert agent_vault.sessions.get(SESSION["id"]).services == [] - assert agent_vault.sessions.list().sessions[0].services == [] + assert agent_vault.sessions.list(project_id=PROJECT_ID).sessions[0].services == [] @pytest.mark.parametrize("status_code, payload, explanation", [ @@ -424,7 +426,7 @@ def test_sessions_create_keeps_the_servers_explanation_when_it_refuses_services( api, agent_vault, json_response, status_code, payload, explanation): api.session.post.return_value = json_response(payload, status_code) with pytest.raises(APIError) as raised: - agent_vault.sessions.create(services=[INLINE_SERVICE_INPUT]) + agent_vault.sessions.create(services=[INLINE_SERVICE_INPUT], project_id=PROJECT_ID) assert raised.value.status_code == status_code assert explanation in str(raised.value) assert api.session.post.call_count == 1 @@ -511,8 +513,8 @@ def test_api_error_is_exported_from_the_package_root(): @pytest.mark.parametrize("call", [ lambda agent_vault: agent_vault.proxies.delete(PROXY["id"]), lambda agent_vault: agent_vault.access_bundles.delete(BUNDLE["id"]), - lambda agent_vault: agent_vault.delegations.delete(MACHINE_IDENTITY_ID), - lambda agent_vault: agent_vault.delegations.remove_subjects(MACHINE_IDENTITY_ID, user_ids=[ON_BEHALF_OF["id"]]), + lambda agent_vault: agent_vault.delegations.delete(MACHINE_IDENTITY_ID, project_id=PROJECT_ID), + lambda agent_vault: agent_vault.delegations.remove_subjects(MACHINE_IDENTITY_ID, user_ids=[ON_BEHALF_OF["id"]], project_id=PROJECT_ID), ], ids=["proxies.delete", "access_bundles.delete", "delegations.delete", "delegations.remove_subjects"]) def test_deletes_are_not_retried_once_the_request_may_have_reached_the_server(api, agent_vault, call): # After a lost response, a retry would fail with a 404 for a delete that worked. @@ -522,8 +524,6 @@ def test_deletes_are_not_retried_once_the_request_may_have_reached_the_server(ap assert api.session.delete.call_count == 1 -PROJECT_ID = "4d6f8a1c-2e3b-4c5d-9e7f-1a2b3c4d5e6f" - LISTS = [ ("proxies.list", lambda agent_vault, **kwargs: agent_vault.proxies.list(**kwargs), {"proxies": [], "totalCount": 0}), ("sessions.list", lambda agent_vault, **kwargs: agent_vault.sessions.list(**kwargs), {"sessions": [], "totalCount": 0}), @@ -566,14 +566,6 @@ def test_lists_send_the_project_id_in_the_query(api, agent_vault, json_response, assert api.session.get.call_args.kwargs["params"]["projectId"] == PROJECT_ID -@pytest.mark.parametrize("call, payload", [(call, payload) for _, call, payload in LISTS], ids=[name for name, _, _ in LISTS]) -def test_lists_leave_out_the_project_id_so_the_server_picks_the_project(api, agent_vault, json_response, call, payload): - api.session.get.return_value = json_response(payload) - call(agent_vault) - # requests drops query parameters whose value is None. - assert api.session.get.call_args.kwargs["params"].get("projectId") is None - - @pytest.mark.parametrize("call, payload", [(call, payload) for _, call, payload in CREATES], ids=[name for name, _, _ in CREATES]) def test_creates_send_the_project_id_in_the_body(api, agent_vault, json_response, call, payload): api.session.post.return_value = json_response(payload) @@ -582,13 +574,6 @@ def test_creates_send_the_project_id_in_the_body(api, agent_vault, json_response assert api.session.post.call_args.kwargs.get("params") is None -@pytest.mark.parametrize("call, payload", [(call, payload) for _, call, payload in CREATES], ids=[name for name, _, _ in CREATES]) -def test_creates_leave_out_the_project_id_so_the_server_picks_the_project(api, agent_vault, json_response, call, payload): - api.session.post.return_value = json_response(payload) - call(agent_vault) - assert "projectId" not in api.session.post.call_args.kwargs["json"] - - @pytest.mark.parametrize("verb, call", [(verb, call) for _, verb, call in DELEGATION_WRITES], ids=[name for name, _, _ in DELEGATION_WRITES]) def test_delegation_writes_send_the_project_id_in_the_query_and_not_the_body(api, agent_vault, json_response, verb, call): @@ -601,7 +586,17 @@ def test_delegation_writes_send_the_project_id_in_the_query_and_not_the_body(api assert "projectId" not in (sent["json"] or {}) -def test_a_project_id_left_out_of_the_query_is_not_sent(): - prepared = requests.Request("PUT", "https://infisical.example.com/api/v1/agent-vault/delegations/id", - params={"projectId": None}).prepare() - assert prepared.url == "https://infisical.example.com/api/v1/agent-vault/delegations/id" +@pytest.mark.parametrize("call", [call for _, call, _ in LISTS + CREATES] + [call for _, _, call in DELEGATION_WRITES], + ids=[name for name, _, _ in LISTS + CREATES + DELEGATION_WRITES]) +def test_the_project_id_is_required(api, agent_vault, call): + with pytest.raises(TypeError, match="project_id"): + call(agent_vault) + for verb in ("get", "post", "put", "delete"): + getattr(api.session, verb).assert_not_called() + + +def test_the_project_id_cannot_be_passed_by_position(api, agent_vault): + # Keyword-only, so a positional argument can't land in project_id by mistake. + with pytest.raises(TypeError): + agent_vault.delegations.get(MACHINE_IDENTITY_ID, PROJECT_ID) + api.session.get.assert_not_called() From 831e8692c2de61b1a3f652b9ed1178e8ac4882f4 Mon Sep 17 00:00:00 2001 From: saif <11242541+saifsmailbox98@users.noreply.github.com> Date: Fri, 6 Nov 2026 02:22:44 +0530 Subject: [PATCH 4/4] fix(agent-vault): keep the project a created session belongs to --- infisical_sdk/api_types.py | 1 + tests/test_agent_vault.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/infisical_sdk/api_types.py b/infisical_sdk/api_types.py index 7528a35..80eb8f1 100644 --- a/infisical_sdk/api_types.py +++ b/infisical_sdk/api_types.py @@ -615,6 +615,7 @@ class CreatedAgentVaultSession(BaseModel): id: str token: str createdAt: str + projectId: Optional[str] = None label: Optional[str] = None expiresAt: Optional[str] = None accessBundle: Optional[AgentVaultSessionAccessBundle] = None diff --git a/tests/test_agent_vault.py b/tests/test_agent_vault.py index 3c75228..99c3f9d 100644 --- a/tests/test_agent_vault.py +++ b/tests/test_agent_vault.py @@ -169,6 +169,12 @@ def test_sessions_create_sends_one_access_bundle(api, agent_vault, json_response assert session.accessBundle.name == "release" + +def test_sessions_create_reads_the_project_the_session_belongs_to(api, agent_vault, json_response): + created = {"id": SESSION["id"], "projectId": PROJECT_ID, "token": "agv_token", "createdAt": SESSION["createdAt"]} + api.session.post.return_value = json_response({"session": created}) + assert agent_vault.sessions.create(access_bundle="release", project_id=PROJECT_ID).projectId == PROJECT_ID + def test_sessions_create_leaves_out_ttl_so_the_server_picks_the_default(api, agent_vault, json_response): api.session.post.return_value = json_response({"session": dict(SESSION, token="token")}) agent_vault.sessions.create(access_bundle="release", project_id=PROJECT_ID)