oed-cli 0.2.3__tar.gz → 0.3.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 (42) hide show
  1. {oed_cli-0.2.3 → oed_cli-0.3.0}/LICENSE +17 -17
  2. oed_cli-0.3.0/PKG-INFO +457 -0
  3. oed_cli-0.3.0/README.md +412 -0
  4. {oed_cli-0.2.3 → oed_cli-0.3.0}/pyproject.toml +28 -2
  5. {oed_cli-0.2.3 → oed_cli-0.3.0}/src/oed_cli/__init__.py +1 -1
  6. {oed_cli-0.2.3 → oed_cli-0.3.0}/src/oed_cli/__main__.py +6 -6
  7. oed_cli-0.3.0/src/oed_cli/auth.py +1711 -0
  8. {oed_cli-0.2.3 → oed_cli-0.3.0}/src/oed_cli/cli.py +21 -2
  9. oed_cli-0.3.0/src/oed_cli/defaults.toml +44 -0
  10. oed_cli-0.3.0/src/oed_cli/discovery.py +248 -0
  11. {oed_cli-0.2.3 → oed_cli-0.3.0}/src/oed_cli/dynamic.py +239 -17
  12. {oed_cli-0.2.3 → oed_cli-0.3.0}/src/oed_cli/errors.py +45 -45
  13. oed_cli-0.3.0/src/oed_cli/http.py +418 -0
  14. {oed_cli-0.2.3 → oed_cli-0.3.0}/src/oed_cli/invoke.py +179 -6
  15. {oed_cli-0.2.3 → oed_cli-0.3.0}/src/oed_cli/main.py +258 -26
  16. oed_cli-0.3.0/src/oed_cli.egg-info/PKG-INFO +457 -0
  17. {oed_cli-0.2.3 → oed_cli-0.3.0}/src/oed_cli.egg-info/SOURCES.txt +7 -1
  18. oed_cli-0.3.0/src/oed_cli.egg-info/requires.txt +31 -0
  19. {oed_cli-0.2.3 → oed_cli-0.3.0}/tests/test_ag_auth_cli.py +176 -153
  20. oed_cli-0.3.0/tests/test_allowlist.py +867 -0
  21. oed_cli-0.3.0/tests/test_auth.py +152 -0
  22. oed_cli-0.3.0/tests/test_cli.py +1117 -0
  23. {oed_cli-0.2.3 → oed_cli-0.3.0}/tests/test_dynamic.py +520 -96
  24. oed_cli-0.3.0/tests/test_http_socks.py +62 -0
  25. {oed_cli-0.2.3 → oed_cli-0.3.0}/tests/test_invoke.py +496 -297
  26. oed_cli-0.3.0/tests/test_logging.py +237 -0
  27. oed_cli-0.3.0/tests/test_secure_storage.py +586 -0
  28. oed_cli-0.3.0/tests/test_spec_integrity.py +624 -0
  29. oed_cli-0.2.3/PKG-INFO +0 -440
  30. oed_cli-0.2.3/README.md +0 -406
  31. oed_cli-0.2.3/src/oed_cli/auth.py +0 -176
  32. oed_cli-0.2.3/src/oed_cli/discovery.py +0 -168
  33. oed_cli-0.2.3/src/oed_cli/http.py +0 -187
  34. oed_cli-0.2.3/src/oed_cli.egg-info/PKG-INFO +0 -440
  35. oed_cli-0.2.3/src/oed_cli.egg-info/requires.txt +0 -8
  36. oed_cli-0.2.3/tests/test_auth.py +0 -132
  37. oed_cli-0.2.3/tests/test_cli.py +0 -294
  38. {oed_cli-0.2.3 → oed_cli-0.3.0}/setup.cfg +0 -0
  39. {oed_cli-0.2.3 → oed_cli-0.3.0}/src/oed_cli/py.typed +0 -0
  40. {oed_cli-0.2.3 → oed_cli-0.3.0}/src/oed_cli.egg-info/dependency_links.txt +0 -0
  41. {oed_cli-0.2.3 → oed_cli-0.3.0}/src/oed_cli.egg-info/entry_points.txt +0 -0
  42. {oed_cli-0.2.3 → oed_cli-0.3.0}/src/oed_cli.egg-info/top_level.txt +0 -0
@@ -1,17 +1,17 @@
1
- Apache License
2
- Version 2.0, January 2004
3
- http://www.apache.org/licenses/
4
-
5
- Copyright 2026 oed-cli contributors
6
-
7
- Licensed under the Apache License, Version 2.0 (the "License");
8
- you may not use this file except in compliance with the License.
9
- You may obtain a copy of the License at
10
-
11
- http://www.apache.org/licenses/LICENSE-2.0
12
-
13
- Unless required by applicable law or agreed to in writing, software
14
- distributed under the License is distributed on an "AS IS" BASIS,
15
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- See the License for the specific language governing permissions and
17
- limitations under the License.
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2026 oed-cli contributors
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
oed_cli-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,457 @@
1
+ Metadata-Version: 2.4
2
+ Name: oed-cli
3
+ Version: 0.3.0
4
+ Summary: oed — openEuler Infra command line. Auto-discovered, AI-friendly.
5
+ Author: oed-cli contributors
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://atomgit.com/openeuler/oed-cli
8
+ Project-URL: Documentation, https://atomgit.com/openeuler/oed-cli/blob/master/docs/cli-design.md
9
+ Project-URL: Bug Tracker, https://atomgit.com/openeuler/oed-cli/issues
10
+ Keywords: openEuler,cli,discovery,ai-agent,openapi
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: click>=8.1
27
+ Requires-Dist: httpx>=0.27
28
+ Requires-Dist: msal>=1.20
29
+ Requires-Dist: keyring>=24
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=8.0; extra == "dev"
32
+ Requires-Dist: ruff>=0.5; extra == "dev"
33
+ Requires-Dist: build>=1.0; extra == "dev"
34
+ Requires-Dist: twine>=5.0; extra == "dev"
35
+ Provides-Extra: socks
36
+ Requires-Dist: socksio>=1.0; extra == "socks"
37
+ Provides-Extra: os-keyring-windows
38
+ Requires-Dist: pywin32-ctypes; sys_platform == "win32" and extra == "os-keyring-windows"
39
+ Provides-Extra: os-keyring-linux
40
+ Requires-Dist: secretstorage; sys_platform == "linux" and extra == "os-keyring-linux"
41
+ Provides-Extra: os-keyring
42
+ Requires-Dist: pywin32-ctypes; sys_platform == "win32" and extra == "os-keyring"
43
+ Requires-Dist: secretstorage; sys_platform == "linux" and extra == "os-keyring"
44
+ Dynamic: license-file
45
+
46
+ # oed-cli
47
+
48
+ > **oed** — one CLI for openEuler community services. Auto-discovered, JSON-first, AI-friendly. Built for humans and LLM agents.
49
+
50
+ [![Python](https://img.shields.io/badge/python-3.10+-blue)](https://www.python.org) [![License](https://img.shields.io/badge/license-Apache--2.0-green)](https://gitcode.com/openeuler/oed-cli/tree/master/LICENSE) [![PyPI](https://img.shields.io/pypi/v/oed-cli)](https://pypi.org/project/oed-cli/)
51
+
52
+ `oed` doesn't ship a static list of commands. It reads the openEuler Infra Discovery Service at runtime and builds its entire command surface dynamically. When a new service ships, `oed` picks it up automatically — no upgrade required.
53
+
54
+ ## Why oed?
55
+
56
+ `oed` exists to solve one specific problem: a CLI that talks to _many evolving services_ without multiplying that complexity. Shipping a hand-written client per service per version is exactly the versioning pressure that the Zylos API versioning research warns against — and for AI agent consumers that pressure is acute: a renamed field silently breaks a tool call, a new required parameter crashes an otherwise healthy workflow. `oed` sidesteps the whole problem by being one client that **discovers every service at runtime**, so the only thing that has to be versioned is the gateway's OpenAPI spec itself.
57
+
58
+ - **Zero boilerplate.** No copy-pasted OpenAPI clients, no per-service SDKs, no `--data` to escape, no `User-Agent` headers to remember.
59
+ - **Runtime discovery.** The `oed cve --help` list you saw above is built from `https://api-gateway.osinfra.cn/discovery/apis` on every call. New services, new endpoints, and new schema fields show up without an `oed` upgrade. A 10-minute cache keeps CI bursts cheap; `oed cache refresh` forces an immediate re-fetch when you know the gateway just shipped.
60
+ - **AI-friendly output.** Single JSON object on stdout, deterministic exit codes (`0` success · `1` user · `2` network · `3` upstream · `4` not found). Logs and progress go to stderr so `| jq` is always safe.
61
+ - **Claude / Cursor ready.** Ships with `.claude/skills/oed-cli/SKILL.md` so agents know how to use it without a custom prompt.
62
+
63
+ ## Install
64
+
65
+ ```
66
+ pip install oed-cli
67
+ ```
68
+
69
+ Or in an isolated environment (recommended for CI):
70
+
71
+ ```
72
+ pipx install oed-cli
73
+ ```
74
+
75
+ From source:
76
+
77
+ ```
78
+ git clone https://gitcode.com/openeuler/oed-cli && cd oed-cli
79
+ pip install -e .
80
+ ```
81
+
82
+ Verify:
83
+
84
+ ```
85
+ oed --version # → oed, version 0.3.0
86
+ ```
87
+
88
+ ## Quick start: install, then call
89
+
90
+ ```
91
+ pip install oed-cli
92
+ oed cve getSecurityNoticeByCveId --cve-id CVE-2019-10082
93
+ ```
94
+
95
+ ### (optional) Call an AtomGit operation — store a token once
96
+
97
+ Operations on the `ag` (AtomGit) service authenticate via an `access_token` query parameter. Store a personal access token once, and every `oed ag ...` call injects it automatically:
98
+
99
+ ```
100
+ oed ag login
101
+ ```
102
+
103
+ Then `oed ag listAuthenticatedUserIssues` just works — no per-call token flag needed. Full details (`oed ag login --token <pat> [--no-verify]`, `--status`, `oed ag logout`, how the token is stored, auto-injection rules) are in the AtomGit (`ag`) authentication section below.
104
+
105
+ That's it. `oed` discovers the service from the gateway, pulls its OpenAPI spec, derives `--cve-id` from the declared `query` parameter, fills WAF-safe browser headers, and ships the request through the production gateway:
106
+
107
+ ```
108
+ {
109
+ "ok": true,
110
+ "status": 200,
111
+ "url": "https://apig.osinfra.cn/cve-security-notice-server/securitynotice/getByCveId",
112
+ "response": {
113
+ "code": 0,
114
+ "result": [
115
+ {
116
+ "cveId": "CVE-2019-10082",
117
+ "affectedProduct": "openEuler-20.03-LTS",
118
+ "affectedComponent": "httpd-2.4.34-18",
119
+ "announcementTime": "2020-05-13",
120
+ ...
121
+ }
122
+ ]
123
+ }
124
+ }
125
+ ```
126
+
127
+ Output is plain JSON on stdout — pipe straight into `jq`:
128
+
129
+ ```
130
+ oed cve getSecurityNoticeByCveId --cve-id CVE-2019-10082 \
131
+ | jq '.response.result[0] | {cveId, affectedProduct, affectedComponent}'
132
+ ```
133
+
134
+ By default `oed` is quiet: only the result JSON reaches stdout, and errors reach stderr — so the pipe above is always clean. Diagnostics and human-progress prompts are level-gated:
135
+
136
+ - `oed --log-level info <…>` — also show human prompts (e.g. the device-flow "visit URL" line).
137
+ - `oed --log-level debug <…>` — also show HTTP request/response, discovery cache, and token-refresh diagnostics.
138
+ - `-v` = `--log-level debug`, `-q` = `--log-level warning` (the default).
139
+ - `OED_LOG_LEVEL=debug|info|warning` sets it without a flag.
140
+
141
+ The flag may appear anywhere in the command line and applies to both built-in commands (`oed info`, `oed auth login`) and dynamic dispatch (`oed <service> <method>`).
142
+
143
+ ```
144
+ {
145
+ "cveId": "CVE-2019-10082",
146
+ "affectedProduct": "openEuler-20.03-LTS",
147
+ "affectedComponent": "httpd-2.4.34-18"
148
+ }
149
+ ```
150
+
151
+ ### Want to look around first? (optional)
152
+
153
+ ```
154
+ oed info # gateway snapshot: services_total, cache status, community
155
+ oed services # full service list — pick one to call next
156
+ oed <service> --help # list that service's operations
157
+ oed <service> <operation> --help # see every flag for one operation
158
+ oed <service> <operation> --dry-run --… # preview the request, no network
159
+ ```
160
+
161
+ The first `oed <service> <operation>` call after install will pull a fresh discovery feed + that service's OpenAPI spec; subsequent calls within 10 minutes reuse the local cache. Run `oed cache refresh` to force a re-fetch when you know the gateway just shipped something new.
162
+
163
+ ## "What flags does this operation take?" → `--help`
164
+
165
+ Don't guess. Each operation auto-derives its own flags from the OpenAPI schema:
166
+
167
+ ```
168
+ oed cve getSecurityNoticeByCveId --help
169
+ ```
170
+
171
+ ```
172
+ {
173
+ "help_for": "getSecurityNoticeByCveId",
174
+ "operation_id_raw": "getSecurityNoticeByCveId",
175
+ "method": "GET",
176
+ "path": "/cve-security-notice-server/securitynotice/getByCveId",
177
+ "url": "https://apig.osinfra.cn/cve-security-notice-server/securitynotice/getByCveId",
178
+ "parameters": [
179
+ {
180
+ "name": "cveId",
181
+ "in": "query",
182
+ "required": true,
183
+ "flag": "--cve-id",
184
+ "alt_flag": "--cveId",
185
+ "type": "string",
186
+ "description": "CVE ID"
187
+ }
188
+ ],
189
+ "usage": "oed <service> getSecurityNoticeByCveId --cve-id <value> [--dry-run]"
190
+ }
191
+ ```
192
+
193
+ Same idea at the service level — list every operation with one flag each:
194
+
195
+ ```
196
+ oed cve --help | jq '.operations | length'
197
+ # → 37
198
+ ```
199
+
200
+ ## More examples
201
+
202
+ ```
203
+ # Dry-run — preview the request without hitting the network
204
+ oed cve getSecurityNoticeByCveId --cve-id CVE-2019-10082 --dry-run
205
+
206
+ # Date-filtered query — pagination comes back as total/page/size
207
+ oed meeting listMeetings --date 2026-07-29
208
+ # → response.data: [ { "topic": "安全sig例会", "group_name": "security-committee",
209
+ # "date": "2026-07-29", "start": "16:00", "end": "18:00",
210
+ # "join_url": "https://meeting.huaweicloud.com:36443/#/j/985661561", ... } ]
211
+
212
+ # Forum — Discourse `/latest.json`;
213
+ oed forum listLatestTopics --per-page 2
214
+ # → response.topic_list.topics: [ { "title": "《openEuler社区论坛使用指南&规则》",
215
+ # "posts_count": 12, "created_at": "2023-01-16T07:53:30.163Z" }, ... ]
216
+
217
+ # Search — POST JSON body; `keyword` + `lang` are required, pageSize must be 6-49
218
+ oed search multisearchDocByKeyword \
219
+ --json '{"keyword":"软件源安装速度慢怎么办","lang":"zh","page":1,"pageSize":10}'
220
+ # → response.obj.records: [ { "title": "<span>软件下载慢问题</span>",
221
+ # "path": "https://eur.openeuler.openatom.cn/coprs/",
222
+ # "type": "service", "lang": "zh" }, ... ]
223
+ ```
224
+
225
+ ## oneid login (RFC 8628 device flow)
226
+
227
+ Most services behind the openEuler gateway (eulermaker, pkgcontrib, meeting, …) require a **user identity** — not an AtomGit PAT, but your openEuler oneid account. `oed login` obtains it via the [RFC 8628 Device Authorization Grant](https://datatracker.ietf.org/doc/html/rfc8628): no local HTTP server, no `redirect_uri`, no `client_secret`, no port to open on a firewall. It works identically on a laptop, over SSH, and in a container.
228
+
229
+ ### Log in
230
+
231
+ ```
232
+ oed login
233
+ ```
234
+
235
+ `oed` asks openEuler oneid for a device code and prints both the **user code** and the **verification URL** to stderr:
236
+
237
+ ```
238
+ Open https://omapi.osinfra.cn/oneid/oidc/device in a browser.
239
+ Enter the user code: 98R4-GGKW
240
+ Waiting for approval...
241
+ ```
242
+ <img width="846" height="98" alt="oed login 触发后终端输出 user code + verification URL" src="docs/images/oed-login-terminal-output.png" />
243
+ <img width="601" height="626" alt="oneid 设备码输入页" src="docs/images/oed-login-device-input.png" />
244
+ <img width="527" height="712" alt="openEuler 服务授权页(搜索 / CVE / 论坛 / EulerMaker)" src="docs/images/oed-login-approve.png" />
245
+ <img width="500" height="205" alt="授权成功,oed 开始轮询后台" src="docs/images/oed-login-success.png" />
246
+
247
+ Open the URL in **any** browser — the one on this machine, a phone, or a laptop connected to a remote host over SSH — enter the user code, and approve the request with your openEuler account. `oed` polls oneid in the background and, once you approve, stores the resulting `access_token` + `refresh_token` automatically. You never paste a token into the terminal.
248
+
249
+ `oed login` is a top-level shortcut for `oed auth login`; both names stay valid and run the same device flow.
250
+
251
+ ### Where the token lives
252
+
253
+ After a successful login, the token is written to your **OS-native credential store** when one is reachable — macOS Keychain, Windows DPAPI / Credential Manager, Linux SecretService (libsecret). When no keystore is reachable (headless Linux, CI, locked GNOME Keyring), `oed` silently falls back to `<OED_CACHE_DIR>/auth.json` with `0600` perms on POSIX; no user action is required.
254
+
255
+ | Platform | Backend |
256
+ | ---------------------- | ----------------------------------------------- |
257
+ | macOS | Keychain (encrypted) |
258
+ | Windows | DPAPI / Credential Manager (encrypted) |
259
+ | Linux desktop | SecretService / libsecret (encrypted) |
260
+ | Linux headless / CI | `auth.json` 0600 plaintext fallback |
261
+
262
+ > **Why `oed auth status` says `plaintext` on Windows / Linux out of the box** — Python's `keyring` library ships per-platform backends as *optional* extras (so it stays installable on minimal systems). `oed-cli` only depends on `keyring>=24`; for the Windows DPAPI / Linux SecretService backend you also need to install one of:
263
+ >
264
+ > ```
265
+ > pip install "oed-cli[os-keyring-windows]" # Windows DPAPI / Credential Manager
266
+ > pip install "oed-cli[os-keyring-linux]" # Linux SecretService (libsecret)
267
+ > pip install "oed-cli[os-keyring]" # both
268
+ > ```
269
+ >
270
+ > Without the matching backend, `oed` silently uses the 0600 plaintext fallback — nothing is broken, the `backend` field in `oed auth status` will read `"plaintext"`.
271
+
272
+ Inspect the active backend any time with `oed auth status` — the JSON includes a `"backend"` field (`"keyring"` or `"plaintext"`). When keyring is in use, no `auth.json` is written to disk.
273
+
274
+ Once logged in, every subsequent `oed <service> <operation>` call sends the token as `Authorization: Bearer <token>` automatically — no per-call flag. A `401` from the upstream surfaces as an `UpstreamError(kind="unauthorized")` with a hint pointing at `oed auth status`.
275
+
276
+ ### Headless / SSH / CI login
277
+
278
+ On a headless host (no `DISPLAY` / `WAYLAND_DISPLAY`) `oed login` still works — it prints the URL + user code to stderr and keeps polling. You approve from **any** browser elsewhere; the CLI picks up the token when the approval lands. No browser needs to run on the server.
279
+
280
+ To skip the auto-open-the-browser attempt entirely (CI / containers), set:
281
+
282
+ ```
283
+ BROWSER=none oed login
284
+ ```
285
+
286
+ > **Tip** — don't re-run `oed login` while a code is pending. A new run requests a **new** device code and overwrites the one you may already be approving in the browser. If the first code expired, just wait — oneid invalidates it and `oed login` will tell you to start fresh.
287
+
288
+ ### Other auth commands
289
+
290
+ ```
291
+ oed auth status # is a token loaded? (token is redacted as first3...last2; shows backend + allowlist)
292
+ oed login --manual # paste a token from a TTY (rejected in non-TTY contexts)
293
+ oed auth token <bearer> [--cookie "k=v"] # write a token directly — preferred for agents / CI / piped scripts
294
+ oed auth logout # clear the stored token
295
+ ```
296
+
297
+ Runtime precedence: `OED_TOKEN` env wins over the stored token; `OED_COOKIE` does the same for the optional `Cookie` header.
298
+
299
+ ### Per-user service allow-list
300
+
301
+ When you log in via `oed login` (device flow), oneid returns the list of services you've authorized for this CLI. `oed` stores it locally in `auth.json.allowlist` and uses it to gate subsequent `oed <service> ...` invocations — services not on the list are rejected with `kind="service_blocked_by_allowlist"` (exit 1). Update your allow-list in the oneid UI, then re-run `oed login` to refresh the local copy.
302
+
303
+ The check is **fail-open** when `auth.json` is missing, has no `allowlist` field (legacy compat), or the list is `[]` (you explicitly cleared it in oneid). Reserved commands (`auth`, `services`, `info`, `schema`, `cache`, `--help`, `--version`) bypass the gate entirely. `oed auth status` shows the current list.
304
+
305
+ ## AtomGit (`ag`) authentication
306
+
307
+ AtomGit operations authenticate through the `access_token` query parameter declared on their spec. Store a personal access token once, and every `oed ag ...` call uses it automatically:
308
+
309
+ ```
310
+ # Interactive (prompts for the token, never echoes it back)
311
+ oed ag login
312
+
313
+ # Non-interactive — good for CI / scripts
314
+ oed ag login --token <pat>
315
+
316
+ # Skip validating the token against AtomGit before storing
317
+ oed ag login --token <pat> --no-verify
318
+
319
+ # Just report whether a token is configured (no network, no prompt)
320
+ oed ag login --status
321
+
322
+ # Forget the stored token
323
+ oed ag logout
324
+ ```
325
+
326
+ Token storage:
327
+
328
+ - Same keyring-backed store as `oed login`: macOS Keychain / Windows DPAPI / Linux SecretService when an OS keystore is reachable, otherwise a 0600 plaintext fallback file.
329
+ - Lives under a `tokens/` subdir of the cache dir (`tokens/ag.json` when keyring is unavailable); `oed cache clear` never touches credentials.
330
+ - Stored under a separate keyring entry (`ag:ag`) from the `oed login` oneid token, so the two credentials never collide.
331
+
332
+ Auto-injection on real calls:
333
+
334
+ - If the operation declares `access_token` and you don't pass one, the stored token is filled in automatically — `oed ag listAuthenticatedUserIssues` just works.
335
+ - An explicit `--access-token <pat>` always wins over the stored one.
336
+ - If the operation requires a token and none is available anywhere, you get a clear `ag_token_missing` error with a hint, instead of an opaque gateway 401.
337
+ - `--dry-run` and request echo views mask the token as `<stored>` — the real value only ever goes out on the wire.
338
+
339
+ ## Local development
340
+
341
+ ### Clone and install (editable)
342
+
343
+ ```
344
+ git clone https://gitcode.com/openeuler/oed-cli && cd oed-cli
345
+ pip install -e ".[dev]"
346
+ ```
347
+
348
+ `pip install -e .` makes source edits take effect on the next `oed` invocation. Drop it with `pip uninstall oed-cli` when you're done.
349
+
350
+ ### Run the tests (~0.3 s, fully offline)
351
+
352
+ ```
353
+ python -m pytest -q
354
+ ```
355
+
356
+ 58 tests cover v0.1 + v0.2 dispatch, the operation-help cheatsheet, per-parameter flag coercion, the `API_`-prefix alias, the `resolve_runtime_gateway` no-fallback semantics, every exit code path, and v0.4's `ag` token store (DPAPI/base64) + auto-injection. They monkeypatch the discovery layer so no gateway access is needed.
357
+
358
+ ### Smoke-test against the live gateway
359
+
360
+ ```
361
+ # 1) Health check
362
+ oed info
363
+ # → {"ok": true, "services_total": 8, "community": "openeuler", ...}
364
+
365
+ # 2) Real CVE query (canonical end-to-end test)
366
+ oed cve getSecurityNoticeByCveId --cve-id CVE-2019-10082
367
+
368
+ # 3) Dry-run to inspect URL + body without hitting the network
369
+ oed cve getSecurityNoticeByCveId --cve-id 1 --dry-run
370
+
371
+ # 4) Verify the canonical URL routing
372
+ oed cve getSecurityNoticeByCveId --dry-run --cve-id 1 \
373
+ | jq '.url'
374
+ # → "https://apig.osinfra.cn/cve-security-notice-server/securitynotice/getByCveId"
375
+ ```
376
+
377
+ ### Cache debugging
378
+
379
+ ```
380
+ oed cache show # path, size, age, TTL
381
+ oed cache refresh # force re-fetch the discovery feed
382
+ oed cache clear # delete the cache file
383
+ ```
384
+
385
+ Cache lives at `~/.cache/oed-cli/` (XDG) or `C:\Users\<you>\AppData\Local\oed-cli\cache\` on Windows. Override with `OED_CACHE_DIR=...`. Per-service OpenAPI specs are cached next to it under `specs/<community>/<service>.json` with the same 10-minute TTL. The auth file (`auth.json`) and the exchange endpoint's bundled OAuth client credentials are also rooted under the same `OED_CACHE_DIR`.
386
+
387
+ ### Keeping in sync with the gateway
388
+
389
+ `oed` does **not** warm up a fresh discovery feed on every invocation — only the first command in a 10-minute window actually hits the gateway, the rest read from `~/.cache/oed-cli/discovery.json`. That keeps CI scripts that run dozens of `oed` calls from hammering the gateway, and it means `oed --help` and `oed --version` never touch the network.
390
+
391
+ When the gateway adds a new service or operation, refresh the cache by hand:
392
+
393
+ ```
394
+ # Fastest: drop the 10-minute TTL and re-pull the discovery feed
395
+ oed cache refresh
396
+
397
+ # Or, nuke and re-fetch from a known-clean state
398
+ oed cache clear && oed info
399
+
400
+ # Then confirm the new service is now visible
401
+ oed services | jq -r '.[] | .service_name'
402
+ ```
403
+
404
+ The first call to a brand-new service will additionally pull its OpenAPI spec into `specs/<community>/<service>.json` (also 10-minute TTL); after that it's reused like any other spec.
405
+
406
+ **Why this isn't automatic.** openEuler Infra services are added on a weekly-to-quarterly cadence via review, not minute-to-minute. Auto-refreshing on every `oed` invocation would burn a network round-trip per CLI call for no practical benefit. The TTL exists to absorb CI bursts, not to delay visibility of new endpoints.
407
+
408
+ **Future** — `oed whatsnew` (planned) will diff the freshly-pulled feed against the previous cache and print only what changed, so you don't have to eyeball `oed services` output every week.
409
+
410
+ **Cache integrity.** The local cache is plain JSON and lives in a user-writable directory, so a malicious same-user process can in principle rewrite it. `oed` applies three defenses (issue #22): (1) a cached timestamp more than ~5 min in the future is treated as poisoned and discarded — this closes the "set the timestamp ahead so the entry never expires" trick; (2) a runtime `base_url` whose scheme is anything other than `https` (e.g. a poisoned `http://evil.com`) is hard-rejected before any credentials are attached, so a tampered host can't exfiltrate your Bearer token / `ag` PAT over plaintext; (3) cache writes are atomic (tmp + replace) so a crash mid-write can't leave a corrupt file. If you ever see `insecure_base_url`, run `oed cache clear` and retry. Note this is defense-in-depth — a same-user process that can write the cache can typically also read your keyring directly.
411
+
412
+ ### Common errors
413
+
414
+ | Symptom | Cause | Fix |
415
+ | --- | --- | --- |
416
+ | `ModuleNotFoundError: oed_cli` | not installed in env | `pip install -e .` |
417
+ | `oed info` hangs or `waf_block` exit 2 | gateway unreachable / WAF | confirm `curl https://api-gateway.osinfra.cn`; see `context/discoverAPI.md` §6 |
418
+ | Chinese output garbled on Windows | console codepage not UTF-8 | `chcp 65001`, or pipe `| python`, or `PYTHONIOENCODING=utf-8 oed …` |
419
+ | `error="spec_missing"` (exit 4) on a known service | upstream hasn't published the spec yet | wait for the gateway-side OpenAPI yaml; nothing to do on the oed side |
420
+ | `error="ag_token_missing"` on an `ag` call | operation needs a token, none stored | `oed ag login` (or pass `--access-token <pat>`) |
421
+ | `error="body_fields_via_params"` (exit 1) on a POST | body fields were passed via `--params` (which only covers query/path) | resend the fields with `--json '{...}'` — `oed <service> <op> --help` lists the body schema |
422
+ | `error="gateway_managed_param"` (exit 1) on a `forum` call | `Api-Key` / `Api-Username` were passed (`--api-key` or `--params`) | drop them — `oed` auto-fills both placeholder headers on every `forum` call and the gateway converts them |
423
+ | `error="service_blocked_by_allowlist"` (exit 1) | the service is not on your oneid allow-list | update your allow-list in oneid, then `oed login` to refresh the local copy |
424
+ | `error="unauthorized"` (exit 3) on an authenticated call | token missing / expired / wrong role | `oed auth status`; re-run `oed login` |
425
+ | A `cve` call exits 2 (`waf_block`) | spec points to a `.test.osinfra.cn` host | already handled — `oed` reads `base_url` from the discovery feed (no fallback) and ignores the spec's `x-apigateway-backend.httpEndpoints.address` for the host |
426
+
427
+ ### Offline mode
428
+
429
+ `oed --help`, `oed info` (uses cached feed), `pytest`, and any command against a service whose spec is in the local cache all work without network. To run `oed` from source without installing:
430
+
431
+ ```
432
+ python -m oed_cli --help
433
+ # or
434
+ python -c "from oed_cli.main import main; sys.argv = ['oed','--help']; main()"
435
+ ```
436
+
437
+ ## Documentation
438
+
439
+ - Design doc — architecture, command contract, exit codes, packaging, roadmap.
440
+ - Local testing guide — install, smoke test, every per-parameter flag demo.
441
+ - Discovery API reference — the data source `oed` consumes, plus WAF caveats.
442
+
443
+ ## Contributing
444
+
445
+ Issues and patches welcome on [gitcode.com/openeuler/oed-cli](https://gitcode.com/openeuler/oed-cli).
446
+
447
+ Dev install:
448
+
449
+ ```
450
+ pip install -e ".[dev]"
451
+ pytest
452
+ ruff check src tests
453
+ ```
454
+
455
+ ## License
456
+
457
+ Apache-2.0. See [LICENSE](https://gitcode.com/openeuler/oed-cli/tree/master/LICENSE).