virtual-key-mapper 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. virtual_key_mapper-0.1.0/LICENSE +202 -0
  2. virtual_key_mapper-0.1.0/PKG-INFO +450 -0
  3. virtual_key_mapper-0.1.0/README.md +425 -0
  4. virtual_key_mapper-0.1.0/pyproject.toml +39 -0
  5. virtual_key_mapper-0.1.0/setup.cfg +4 -0
  6. virtual_key_mapper-0.1.0/src/virtual_key_mapper/__init__.py +3 -0
  7. virtual_key_mapper-0.1.0/src/virtual_key_mapper/__main__.py +3 -0
  8. virtual_key_mapper-0.1.0/src/virtual_key_mapper/adapters.py +115 -0
  9. virtual_key_mapper-0.1.0/src/virtual_key_mapper/auth/__init__.py +1 -0
  10. virtual_key_mapper-0.1.0/src/virtual_key_mapper/auth/forwarded.py +155 -0
  11. virtual_key_mapper-0.1.0/src/virtual_key_mapper/auth/litellm_oauth.py +404 -0
  12. virtual_key_mapper-0.1.0/src/virtual_key_mapper/auth/litellm_sso.py +296 -0
  13. virtual_key_mapper-0.1.0/src/virtual_key_mapper/cli.py +875 -0
  14. virtual_key_mapper-0.1.0/src/virtual_key_mapper/errors.py +2 -0
  15. virtual_key_mapper-0.1.0/src/virtual_key_mapper/gateways/__init__.py +1 -0
  16. virtual_key_mapper-0.1.0/src/virtual_key_mapper/gateways/litellm.py +92 -0
  17. virtual_key_mapper-0.1.0/src/virtual_key_mapper/integrations/__init__.py +1 -0
  18. virtual_key_mapper-0.1.0/src/virtual_key_mapper/integrations/claude.py +371 -0
  19. virtual_key_mapper-0.1.0/src/virtual_key_mapper/integrations/codex.py +190 -0
  20. virtual_key_mapper-0.1.0/src/virtual_key_mapper/integrations/common.py +115 -0
  21. virtual_key_mapper-0.1.0/src/virtual_key_mapper/integrations/opencode.py +320 -0
  22. virtual_key_mapper-0.1.0/src/virtual_key_mapper/labels.py +742 -0
  23. virtual_key_mapper-0.1.0/src/virtual_key_mapper/profiles.py +339 -0
  24. virtual_key_mapper-0.1.0/src/virtual_key_mapper/registry.py +74 -0
  25. virtual_key_mapper-0.1.0/src/virtual_key_mapper/repositories.py +263 -0
  26. virtual_key_mapper-0.1.0/src/virtual_key_mapper/resolve.py +238 -0
  27. virtual_key_mapper-0.1.0/src/virtual_key_mapper/rules.py +297 -0
  28. virtual_key_mapper-0.1.0/src/virtual_key_mapper/shells.py +116 -0
  29. virtual_key_mapper-0.1.0/src/virtual_key_mapper/storage.py +234 -0
  30. virtual_key_mapper-0.1.0/src/virtual_key_mapper/transport.py +130 -0
  31. virtual_key_mapper-0.1.0/src/virtual_key_mapper.egg-info/PKG-INFO +450 -0
  32. virtual_key_mapper-0.1.0/src/virtual_key_mapper.egg-info/SOURCES.txt +56 -0
  33. virtual_key_mapper-0.1.0/src/virtual_key_mapper.egg-info/dependency_links.txt +1 -0
  34. virtual_key_mapper-0.1.0/src/virtual_key_mapper.egg-info/entry_points.txt +2 -0
  35. virtual_key_mapper-0.1.0/src/virtual_key_mapper.egg-info/requires.txt +2 -0
  36. virtual_key_mapper-0.1.0/src/virtual_key_mapper.egg-info/top_level.txt +1 -0
  37. virtual_key_mapper-0.1.0/tests/test_adapters.py +174 -0
  38. virtual_key_mapper-0.1.0/tests/test_agents_cli.py +461 -0
  39. virtual_key_mapper-0.1.0/tests/test_architecture.py +68 -0
  40. virtual_key_mapper-0.1.0/tests/test_claude_integration.py +417 -0
  41. virtual_key_mapper-0.1.0/tests/test_claude_native.py +230 -0
  42. virtual_key_mapper-0.1.0/tests/test_codex_opencode.py +426 -0
  43. virtual_key_mapper-0.1.0/tests/test_devcontainer.py +118 -0
  44. virtual_key_mapper-0.1.0/tests/test_forwarded.py +162 -0
  45. virtual_key_mapper-0.1.0/tests/test_labels.py +291 -0
  46. virtual_key_mapper-0.1.0/tests/test_labels_cli.py +572 -0
  47. virtual_key_mapper-0.1.0/tests/test_oauth.py +377 -0
  48. virtual_key_mapper-0.1.0/tests/test_parallel_labels.py +106 -0
  49. virtual_key_mapper-0.1.0/tests/test_profiles.py +174 -0
  50. virtual_key_mapper-0.1.0/tests/test_release.py +36 -0
  51. virtual_key_mapper-0.1.0/tests/test_repositories.py +260 -0
  52. virtual_key_mapper-0.1.0/tests/test_resolve.py +269 -0
  53. virtual_key_mapper-0.1.0/tests/test_rules.py +297 -0
  54. virtual_key_mapper-0.1.0/tests/test_sessions.py +553 -0
  55. virtual_key_mapper-0.1.0/tests/test_storage.py +259 -0
  56. virtual_key_mapper-0.1.0/tests/test_tags.py +166 -0
  57. virtual_key_mapper-0.1.0/tests/test_tools_native.py +319 -0
  58. virtual_key_mapper-0.1.0/tests/test_vault_native.py +137 -0
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright 2026 cbs Corporate Business Solutions Unternehmensberatung GmbH
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
@@ -0,0 +1,450 @@
1
+ Metadata-Version: 2.4
2
+ Name: virtual-key-mapper
3
+ Version: 0.1.0
4
+ Summary: Short-lived keys from your company gateway, labelled with the repository you work in, for every tool you use.
5
+ Author: cbs Corporate Business Solutions Unternehmensberatung GmbH
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/cbs-consulting/virtual-key-mapper
8
+ Project-URL: Issues, https://github.com/cbs-consulting/virtual-key-mapper/issues
9
+ Project-URL: Changelog, https://github.com/cbs-consulting/virtual-key-mapper/blob/main/CHANGELOG.md
10
+ Keywords: llm,gateway,litellm,sso,virtual-keys,cost-attribution,claude-code,codex,opencode,copilot
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Topic :: Software Development
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: filelock==4.0.9
23
+ Requires-Dist: keyring==25.7.0
24
+ Dynamic: license-file
25
+
26
+ <div align="center">
27
+
28
+ <img src="https://raw.githubusercontent.com/cbs-consulting/virtual-key-mapper/main/.github/assets/logo.svg" alt="" width="120" />
29
+
30
+ # cbs Virtual Key Mapper
31
+
32
+ **Short-lived keys from your company gateway, labelled with the repository you work in, for every tool you use.**
33
+
34
+ [![PyPI](https://img.shields.io/pypi/v/virtual-key-mapper?logo=pypi&logoColor=white)](https://pypi.org/project/virtual-key-mapper/)
35
+ [![VS Code Marketplace](https://img.shields.io/visual-studio-marketplace/v/cbs-consulting.vkm?label=VS%20Code)](https://marketplace.visualstudio.com/items?itemName=cbs-consulting.vkm)
36
+ [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
37
+ [![Python](https://img.shields.io/badge/python-%3E%3D3.11-3776AB?logo=python&logoColor=white)](pyproject.toml)
38
+ [![Platforms](https://img.shields.io/badge/platforms-Windows%20%7C%20macOS%20%7C%20Linux-0F4C5C)](#-what-works-where)
39
+ [![Gateway](https://img.shields.io/badge/gateway-LiteLLM-0F4C5C)](docs/architecture.md#11-gateway-requirements)
40
+
41
+ </div>
42
+
43
+ ---
44
+
45
+ `vkm` signs you in to your company's gateway once, with its single sign-on. From then on your
46
+ tools get short-lived keys from it, and every request carries the **labels** of the
47
+ repository you are working in: key/value pairs such as `customer = "acme"` and
48
+ `project = "webshop"`. Spend is attributed by label without anyone pasting API keys into
49
+ config files, and nothing goes out while a required label has no value.
50
+
51
+ `vkm` does not depend on a particular gateway or tool. Gateways, sign-in methods and tools
52
+ are separate adapters ([docs/adapters.md](docs/adapters.md)), so new ones can be added
53
+ without touching the rest.
54
+
55
+ ## ✨ What works where
56
+
57
+ ### Gateways and sign-in
58
+
59
+ | Gateway | Sign-in | What the gateway receives |
60
+ | --- | --- | --- |
61
+ | [LiteLLM](https://github.com/BerriAI/litellm) | Its native CLI single sign-on in the browser, with team choice. The refresh token stays in the OS vault. | The SSO user and team (from the token), the labels as request tags `x-litellm-tags` (by default one `KEY:VALUE` tag per label, e.g. `customer:acme`), and `x-litellm-spend-logs-metadata` = `{"labels": {"customer": "acme"}, "sources": {"customer": "explicit\|env\|local\|file\|rules"}, "repository": "github.com/org/repo"}` (repository only inside a Git checkout with an origin). |
62
+ | LiteLLM, from a VS Code dev container, SSH or WSL window | `forwarded`: the two VS Code extensions relay short-lived keys from the sign-in on your own computer; the refresh token never leaves it. | The same. |
63
+
64
+ More gateways and sign-in methods are adapters ([docs/adapters.md](docs/adapters.md)).
65
+
66
+ ### Tools
67
+
68
+ | Tool | Connected with | Labels | Key renewal | While a required label has no value |
69
+ | --- | --- | --- | --- | --- |
70
+ | Claude Code CLI | `vkm setup claude` | per repository, checked before each prompt | ✅ | the prompt is blocked |
71
+ | Claude Code for VS Code | `vkm setup claude` + `"claudeCode.disableLoginPrompt": true` | per repository, checked before each prompt | ✅ | the prompt is blocked |
72
+ | Codex CLI | `vkm setup codex` + the shell hook | of the folder where Codex starts | ✅ | no key is sent; the gateway refuses |
73
+ | OpenCode v1 | `vkm setup opencode` | per request, of the folder OpenCode runs in | ✅ | the request is blocked |
74
+ | OpenCode v2 | `vkm run opencode` (`setup` refuses v2) | fixed at launch | ❌ key from launch time; restart when it expires | as for `vkm run` |
75
+ | GitHub Copilot Chat in VS Code | the Virtual Key Mapper extension | per workspace folder, shown in the status bar | ✅ | blocked, with an "Assign labels" button that runs `vkm repo bind` |
76
+ | any of the above, one launch | `vkm run [--label KEY=VALUE]... TOOL` | fixed at launch (or the ones you give) | ✅ (OpenCode v2: ❌) | asks in a terminal for each missing label and saves the answers; blocked without a terminal or outside Git |
77
+ | Codex for VS Code | not supported yet | | | |
78
+ | Copilot inline completions | not possible: they always use GitHub's models | | | |
79
+
80
+ ### Platforms
81
+
82
+ ✅ = tested in CI on every push, with the real tools (pinned in `ci/agents`, plus a weekly
83
+ run with their latest releases) against a local test gateway. ☑️ = supported and checked by
84
+ hand with the [manual checklist](docs/testing.md#manual-checklist).
85
+
86
+ | | Windows | macOS | Linux | Dev container | SSH, WSL |
87
+ | --- | --- | --- | --- | --- | --- |
88
+ | Claude Code CLI, Codex CLI, OpenCode v1 | ✅ (interactive Codex ☑️) | ✅ | ✅ | ✅ Claude Code and Codex, ☑️ OpenCode | ☑️ |
89
+ | Claude Code for VS Code, Copilot Chat, OpenCode v2 | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ |
90
+ | Credential vault | ✅ Credential Manager | ✅ Keychain | ✅ Secret Service (GNOME Keyring), ☑️ KWallet | not needed | not needed |
91
+ | Shell hooks | ✅ PowerShell 7, bash; ☑️ Windows PowerShell 5.1 and PCs that block scripts (`shell-init pwsh --terminal`) | ✅ bash, zsh, fish, PowerShell 7 | ✅ bash, zsh, fish, PowerShell 7 | as on Linux | as on Linux |
92
+ | VS Code extensions | ✅ in a VS Code extension host | ✅ in an extension host, with the relay | ✅ in an extension host, with the relay | ✅ relay in a real dev container | ☑️ |
93
+
94
+ ### Choosing labels
95
+
96
+ Each label is resolved on its own, so `customer` can come from a central rule and `project`
97
+ from a committed file. For each key the first source with a value wins:
98
+
99
+ | Source | Who sets it | Applies to |
100
+ | --- | --- | --- |
101
+ | `vkm run --label KEY=VALUE` | you | that launch |
102
+ | `VKM_LABEL_<KEY>` (e.g. `VKM_LABEL_CUSTOMER`) | you | every process started with the variable set |
103
+ | `vkm repo bind --label KEY=VALUE` | you | clones of that repository on this computer (same origin, or it as upstream), only for you |
104
+ | a committed `.vkm` file (`vkm repo bind --shared --label KEY=VALUE` writes it at the repository root) | the team | that folder and everything below it, unless a nearer `.vkm` exists |
105
+ | central rules (a JSON file or URL in the profile) | your organisation | repositories matching a pattern, e.g. `github.com/acme-*/**` |
106
+
107
+ Which labels exist and which are required is set by your organisation's central rules
108
+ document or by your profile ([Labels](#-labels)).
109
+
110
+ ## 🚀 Quick start
111
+
112
+ Needs Python 3.11 or newer and an OS credential vault.
113
+
114
+ ```bash
115
+ uv tool install virtual-key-mapper # or: pipx install virtual-key-mapper
116
+ ```
117
+
118
+ Update later with `uv tool upgrade virtual-key-mapper` (or `pipx upgrade virtual-key-mapper`).
119
+ From a checkout instead:
120
+
121
+ ```bash
122
+ python -m venv .venv
123
+ .venv/bin/python -m pip install -e . # Windows: .venv\Scripts\python.exe -m pip install -e .
124
+ ```
125
+
126
+ `vkm` is the installed command. If your machine blocks pip-generated `.exe` launchers (some
127
+ Windows policies do), use `python -m virtual_key_mapper` everywhere instead; it accepts the
128
+ same arguments.
129
+
130
+ ```bash
131
+ # 1. Once per machine: a profile for your gateway and your business-unit team, then sign in.
132
+ # If your organisation publishes its label definitions, pass --rules URL instead of --label.
133
+ vkm profile add work --gateway https://llm.company.example --team-id YOUR_TEAM_ID \
134
+ --label customer --required --description "Who the work is billed to"
135
+ vkm login # browser sign-in; choose the same team there
136
+ vkm status # "state": "ready"
137
+
138
+ # 2. Once per machine: connect your tools.
139
+ vkm setup claude --model YOUR_CLAUDE_ALIAS
140
+ vkm setup codex --model YOUR_CODEX_ALIAS
141
+ vkm setup opencode
142
+
143
+ # 3. Once per repository (unless a central rule or a committed .vkm covers it):
144
+ cd ~/src/some-repo
145
+ vkm repo bind --label customer=acme # or plain 'vkm repo bind' to be asked
146
+ vkm repo show
147
+ ```
148
+
149
+ The gateway address is its root URL (no `/v1`); the team ID is the exact ID from the gateway,
150
+ not its display name. Your sign-in renews itself; sign in again only after logging out, a
151
+ long pause or a change of team membership.
152
+ The gateway must be served at its host root with LiteLLM's `PROXY_BASE_URL` set to that URL
153
+ (see [Gateway requirements](docs/architecture.md#11-gateway-requirements)).
154
+
155
+ ## 🧭 Labels
156
+
157
+ A label is a key with a value, such as `customer = "acme"` or `project = "webshop"`. What a
158
+ label means is up to your organisation; `vkm` knows the keys, which of them are required,
159
+ where their values may come from and how the gateway receives them. The full reference is
160
+ [docs/labels.md](docs/labels.md).
161
+
162
+ ### Definitions
163
+
164
+ Labels are defined in your organisation's central rules document (`profile add --rules URL|PATH`
165
+ or `profile update NAME --rules ...`) and, for keys the document does not define, in your profile:
166
+
167
+ ```bash
168
+ vkm profile update work --label project --description "Project within the customer"
169
+ vkm profile update work --label customer --required --value acme=Acme --value globex=Globex --strict
170
+ vkm profile update work --no-label project
171
+ vkm labels list # the labels in effect, where each is defined, and notices
172
+ ```
173
+
174
+ One label per call. `--required`: nothing is sent while it has no value. `--value ID[=NAME]`:
175
+ the choices the picker offers; with `--strict` only those are accepted. `--source NAME`
176
+ (repeatable) limits where values may come from. Keys are 1-32 lowercase letters, digits, `_`
177
+ or `-`, starting with a letter. A profile label for a key the central document defines is
178
+ refused (or, when the document cannot be read at the time, ignored with a notice): the central
179
+ one applies. If no label is defined anywhere, requests carry only the repository, and `setup`,
180
+ `run` and `repo show` say so.
181
+
182
+ ### Values
183
+
184
+ For each defined key, the first source with a value wins:
185
+
186
+ 1. `--label KEY=VALUE` (for `run` and `repo show`);
187
+ 2. the environment variable `VKM_LABEL_<KEY>` (exactly: uppercase, `-` as `_`; empty counts as
188
+ unset; other `VKM_LABEL_*` names are ignored);
189
+ 3. your personal binding: `vkm repo bind --label KEY=VALUE`, stored only for you
190
+ (`repositories.json` on this computer), keyed by the repository's `origin`/`upstream`
191
+ address, so every clone on this computer shares it. It keeps keys you do not name; a
192
+ different value for a key already bound needs `--replace`. `repo unbind` removes the
193
+ binding, `repo unbind --label KEY` one key;
194
+ 4. the nearest committed `.vkm` file, written with `repo bind --shared --label KEY=VALUE`:
195
+ ```toml
196
+ # Labels for AI tool requests in this folder and below (vkm).
197
+ [labels]
198
+ customer = "acme"
199
+ project = "webshop"
200
+ ```
201
+ A file in a parent folder such as `~/src/acme/` covers all repositories below it, and the
202
+ nearest file wins as a whole (a nearer file without `project` does not fall through to a
203
+ parent's). Inside your home folder the search stops below home, so a file in home or above
204
+ it never applies (`repo bind --shared` refuses to write one there); for folders outside home,
205
+ such as `/workspaces` in a dev container or another drive on Windows, it goes up to the root
206
+ of the drive;
207
+ 5. a central rule (below).
208
+
209
+ `--label KEY=` on the command line, `""` in a `.vkm` and `null` in a rule mean "no value here,
210
+ stop searching". `--label` refuses keys that no definition knows (typos); values for unknown keys
211
+ from other sources are not sent, and `repo show` lists them. Values are 1-256 printable ASCII
212
+ characters without `,`, surrounding spaces or a leading `!` or `&`.
213
+
214
+ ### The central rules document
215
+
216
+ ```json
217
+ {
218
+ "version": "2.0",
219
+ "labels": [
220
+ {"key": "customer", "required": true, "description": "Who the work is billed to",
221
+ "values": [{"id": "acme", "name": "Acme"}, {"id": "globex", "name": "Globex"}]},
222
+ {"key": "project", "description": "Project within the customer"}
223
+ ],
224
+ "outputs": {"litellm": {"tags": {"format": ["${customer}", "${customer}:${project}"]}}},
225
+ "rules": [
226
+ {"match": "github.com/acme-*/**", "labels": {"customer": "acme"}},
227
+ {"match": "**", "labels": {"project": null}}
228
+ ]
229
+ }
230
+ ```
231
+
232
+ Rules are tried in order, each against `origin`, then `upstream`; each key takes its value from
233
+ the first matching rule that sets it, so a catch-all below can still supply another label.
234
+ `**` spans path segments, `*`/`?` stay within one. Write the repository as `repo show` prints it
235
+ (lowercase, `%20` decoded to a space). `outputs` are grouped by gateway type, so one document
236
+ can serve profiles on different gateways. A URL is cached for an hour; if a fetch fails or a
237
+ newly published document is invalid, the last good copy is used.
238
+
239
+ ### Required labels
240
+
241
+ A required label without a value blocks every request with a message starting
242
+ `Required labels have no value:`, naming the keys and the ways to set them. `vkm run` and
243
+ `vkm repo bind` in a terminal, and "Assign labels" in VS Code, ask for each missing one and save
244
+ the answers as your personal binding. The picker offers the label's `values`, else the
245
+ gateway's catalogue, else accepts any valid typed value without signing in (`vkm labels values KEY`
246
+ shows which). `vkm repo bind` asks only for labels a personal binding (with `--shared`: a `.vkm`
247
+ file) may set.
248
+ `repo show` tells you each label's value and where it came from.
249
+
250
+ ### What the gateway receives
251
+
252
+ For LiteLLM, by default:
253
+
254
+ ```text
255
+ x-litellm-tags: customer:acme,project:webshop
256
+ x-litellm-spend-logs-metadata: {"labels":{"customer":"acme","project":"webshop"},"repository":"github.com/acme-shop/web","sources":{"customer":"rules","project":"file"}}
257
+ ```
258
+
259
+ The tags format can be changed in the central document (it wins) or in the profile;
260
+ the metadata can be switched off:
261
+
262
+ ```bash
263
+ vkm profile update work --output 'tags=${customer}' --output 'tags=${customer}:${project}'
264
+ vkm profile update work --output-off metadata
265
+ vkm profile update work --output-default tags
266
+ ```
267
+
268
+ An entry that uses an optional label without a value is left out; `$$` is a literal `$`.
269
+ Formats that could make one label's value look like another's are refused, as is any setup in
270
+ which a required label reaches no output. The rules are in
271
+ [docs/labels.md](docs/labels.md#3-outputs).
272
+
273
+ Labels are attribution, not access control: enforce allowed values on the gateway if you need
274
+ that. Gateway settings that break or leak attribution (tag routing, client-side tag rejection,
275
+ header forwarding) and why the picker needs `values` in the central document are listed in
276
+ [Gateway requirements](docs/architecture.md#11-gateway-requirements).
277
+
278
+ ## 🧩 Tools
279
+
280
+ ### Claude Code (CLI and the official VS Code extension)
281
+
282
+ ```bash
283
+ vkm setup claude [--model ALIAS] [--replace] [--remove]
284
+ ```
285
+
286
+ Writes `~/.claude/settings.json` (or `$CLAUDE_CONFIG_DIR`): an `apiKeyHelper` that runs
287
+ `vkm token`, the gateway address, and a prompt hook. Before each prompt the hook
288
+ resolves the labels, writes the resulting headers to the repository's `.claude/settings.local.json`
289
+ (excluded from Git) and asks you once to send the prompt again; prompts are blocked while a
290
+ required label has no value.
291
+ For the VS Code extension also set `"claudeCode.disableLoginPrompt": true` in your VS Code
292
+ user settings. `--remove` undoes exactly what setup wrote.
293
+
294
+ ### Codex CLI
295
+
296
+ ```bash
297
+ vkm setup codex [--model ALIAS] [--remove]
298
+ eval "$(vkm shell-init bash)" # add to ~/.bashrc; also zsh, fish, pwsh
299
+ ```
300
+
301
+ On Windows, add `vkm shell-init pwsh | Out-String | Invoke-Expression` to your PowerShell
302
+ `$PROFILE`. Where the execution policy blocks scripts (common on company PCs, "running scripts is
303
+ disabled on this system"), a profile cannot load; run `vkm shell-init pwsh --terminal`
304
+ instead and paste its command line into Windows Terminal (Settings → your PowerShell profile →
305
+ Command line) or its snippet into your VS Code user settings. Check in a new terminal:
306
+ `Get-Command codex` shows `Function`.
307
+
308
+ Codex reads the label headers from environment variables that the shell hook sets per directory.
309
+ Without them, or while a required label has no value, Codex gets no credential and the gateway refuses the request. The hook also makes
310
+ `codex` start with `--no-daemon`: Codex's shared background daemon would keep the variables of the
311
+ terminal that started it. If one is running from before, stop it once: `codex app-server daemon stop`.
312
+
313
+ ### OpenCode
314
+
315
+ ```bash
316
+ vkm setup opencode [--model ALIAS] [--remove]
317
+ ```
318
+
319
+ Adds a provider with the gateway's models and a small plugin to
320
+ `~/.config/opencode/opencode.json`; the plugin adds a fresh credential and the folder's
321
+ label headers to every request, and blocks requests while a required label has no value. OpenCode v2 is supported only through `vkm run opencode`.
322
+
323
+ ### One-off launches
324
+
325
+ ```bash
326
+ vkm run [--label KEY=VALUE]... [--model ALIAS] claude|codex|opencode [-- TOOL ARGUMENTS]
327
+ ```
328
+
329
+ Starts the tool with child-only configuration, without changing your setup; each `--label`
330
+ sets that key for that process (`--label KEY=` for no value), the others resolve as usual. In a
331
+ terminal it asks for missing required labels and saves the answers as your personal binding.
332
+
333
+ ### GitHub Copilot Chat (VS Code)
334
+
335
+ Install **Virtual Key Mapper for Copilot Chat** (`cbs-consulting.vkm`) from the Extensions view,
336
+ the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=cbs-consulting.vkm).
337
+ For editors without the Marketplace (Cursor, VSCodium), install `vkm.vsix` from the
338
+ [latest release](https://github.com/cbs-consulting/virtual-key-mapper/releases/latest)
339
+ (Extensions → ··· → Install from VSIX):
340
+
341
+ ```bash
342
+ code --install-extension cbs-consulting.vkm
343
+ ```
344
+
345
+ Set `vkm.command` in your VS Code user settings to how the CLI runs, e.g.
346
+ `["/path/to/.venv/bin/python", "-m", "virtual_key_mapper"]`. Pick a "Virtual Key Mapper"
347
+ model in Copilot Chat; the status bar shows the folder's label values (`acme · webshop`),
348
+ missing required labels in warning style, and `no labels` when none are defined; its tooltip
349
+ lists each label with its description and source. If you use VS Code profiles,
350
+ install the extension in the profile you work in. Inline completions and next-edit
351
+ suggestions always use GitHub's models and are not attributed.
352
+
353
+ ## 🐳 Dev Containers, SSH and WSL
354
+
355
+ The container gets short-lived tokens from the sign-in on your own computer; nothing
356
+ long-lived is stored inside it.
357
+
358
+ 1. Local VS Code: install **Virtual Key Mapper: local sign-in** (`cbs-consulting.vkm-local`) and set `vkm.localCommand` in your user
359
+ settings to the local CLI command. Containers get tokens only for your local default
360
+ profile; list others in `vkm.localProfiles` (user settings) to allow them.
361
+ 2. In the container: install the CLI, e.g.
362
+ `uv tool install --python 3.12 virtual-key-mapper`, and install `cbs-consulting.vkm` into the
363
+ container (VS Code offers "Install in Dev Container"; or list it under
364
+ `customizations.vscode.extensions` in `devcontainer.json`). Reload the window.
365
+ 3. The extension creates a container profile that uses your local sign-in
366
+ (`vkm profile list` shows `forwarded`), with your local profile's own label definitions and
367
+ output overrides and its central rules URL. Do not run `login` in the container.
368
+
369
+ All tools work in the container as described above. Central rules given as a local file cannot
370
+ be copied; the extension then warns that the organisation's label definitions are missing in the
371
+ container (set them with `vkm profile update NAME --rules URL|PATH` there). Personal `repo bind`
372
+ bindings made inside a container live in the container.
373
+
374
+ ## 🗂️ Where things are stored
375
+
376
+ | What | Where |
377
+ | --- | --- |
378
+ | Profiles and their labels, personal label bindings, rules cache, tool state | `%LOCALAPPDATA%\virtual-key-mapper` or `~/.config/virtual-key-mapper` (`--home` or `VKM_HOME` to change) |
379
+ | Sign-in (access and refresh token) | the OS credential vault only; never in files |
380
+ | Claude / Codex / OpenCode configuration | each tool's own user configuration, changed only by `setup` |
381
+
382
+ ## ⌨️ Commands
383
+
384
+ `profile add|list|show|use|update`, `login`, `status`, `token [--json]`, `logout`,
385
+ `repo show|bind|unbind|list`, `labels list|values`, `setup claude|codex|opencode`, `run`,
386
+ `shell-init`, and `claude guard` (the hook). `vkm COMMAND --help` explains each.
387
+
388
+ ## 📌 Releases
389
+
390
+ | What | Where | Name |
391
+ | --- | --- | --- |
392
+ | CLI | [PyPI](https://pypi.org/project/virtual-key-mapper/) | `virtual-key-mapper` (command `vkm`) |
393
+ | Copilot Chat extension | [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=cbs-consulting.vkm), `vkm.vsix` on each [GitHub release](https://github.com/cbs-consulting/virtual-key-mapper/releases) | `cbs-consulting.vkm` |
394
+ | Local sign-in extension | [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=cbs-consulting.vkm-local), `vkm-local.vsix` on each GitHub release | `cbs-consulting.vkm-local` |
395
+
396
+ All three share one version. Releases are made by the "Release" workflow in the Actions tab, which
397
+
398
+ 1. runs the Python and extension tests on the commit to release and builds everything,
399
+ 2. requires a `## VERSION - DATE` entry in [`CHANGELOG.md`](CHANGELOG.md),
400
+ 3. waits for a maintainer to approve the `release` environment,
401
+ 4. publishes the package to PyPI (trusted publishing: no token in the repository) and the
402
+ GitHub release `vVERSION` with the changelog entry as notes and the built files attached.
403
+
404
+ The extensions are then uploaded to the Visual Studio Marketplace by hand, so no store token is
405
+ stored anywhere: download `vkm.vsix` and `vkm-local.vsix` from the release, open
406
+ [marketplace.visualstudio.com/manage](https://marketplace.visualstudio.com/manage) → publisher
407
+ `cbs-consulting`, and choose **⋯ → Update** for each extension (the first time: **New extension →
408
+ Visual Studio Code**). The Marketplace accepts a package only for the publisher named inside it.
409
+
410
+ Every change goes into the "Unreleased" section of `CHANGELOG.md` in its pull request; the
411
+ "Changelog entry" check fails otherwise, unless the pull request has the label "no changelog".
412
+ To prepare a release, set the same version in `pyproject.toml`, `src/virtual_key_mapper/__init__.py`
413
+ and both `package.json` files (`npm install` in `vscode-extension/` updates the lockfile), and
414
+ rename "Unreleased" to `VERSION - DATE`; the tests check that they agree. The workflow can be run
415
+ again and skips what is already published; its "dry run" option only checks and builds.
416
+
417
+ ## 🛠️ Contributing
418
+
419
+ Pull requests need two checks: **Test features** (all CI jobs on Windows, macOS and Linux) and
420
+ **Changelog entry**.
421
+
422
+ ```bash
423
+ .venv/bin/python -m unittest discover -s tests -v
424
+ cd vscode-extension && npm test && npm run test:host # extension unit tests; real VS Code host
425
+ cd vscode-extension/local && npm test
426
+ ```
427
+
428
+ Live tests run the real tools against a local fake gateway (no model calls, no secrets). Install
429
+ the pinned versions and point the tests at them:
430
+
431
+ ```bash
432
+ cd ci/agents && npm ci && cd ../..
433
+ export $(node ci/agents/locate.mjs) # sets VKM_TEST_{CLAUDE,CODEX,OPENCODE}_BINARY
434
+ .venv/bin/python -m unittest discover -s tests -v
435
+ ```
436
+
437
+ `VKM_TEST_VAULT=1` also runs `tests/test_vault_native.py` against your real OS vault (uniquely named test entries, removed afterwards).
438
+
439
+ Locally, missing tools and shells skip their tests; CI requires them. CI runs everything on Windows,
440
+ macOS and Linux: the Python suite, the live agents and shell hooks, the real OS vault, both extensions
441
+ in a real VS Code, and a real dev container on Linux; a weekly run tries the newest agent releases.
442
+ Details and the manual checklist: [docs/testing.md](docs/testing.md). How it works and why:
443
+ [docs/architecture.md](docs/architecture.md); extending it: [docs/adapters.md](docs/adapters.md).
444
+
445
+ ## 📄 License
446
+
447
+ Copyright © 2026 cbs Corporate Business Solutions Unternehmensberatung GmbH.
448
+ Licensed under the [Apache License 2.0](LICENSE).
449
+
450
+ <sub>LiteLLM is a project of BerriAI. Claude Code is a product of Anthropic, Codex of OpenAI, OpenCode of SST, GitHub Copilot and VS Code of GitHub and Microsoft. This project is not affiliated with or endorsed by any of them.</sub>