licos-platform-cli 0.5.1__tar.gz → 0.6.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 (19) hide show
  1. {licos_platform_cli-0.5.1 → licos_platform_cli-0.6.0}/PKG-INFO +10 -3
  2. {licos_platform_cli-0.5.1 → licos_platform_cli-0.6.0}/README.md +8 -1
  3. {licos_platform_cli-0.5.1 → licos_platform_cli-0.6.0}/pyproject.toml +2 -2
  4. {licos_platform_cli-0.5.1 → licos_platform_cli-0.6.0}/src/licos_platform_cli/help_commands.py +22 -11
  5. licos_platform_cli-0.6.0/src/licos_platform_cli/identity.py +26 -0
  6. {licos_platform_cli-0.5.1 → licos_platform_cli-0.6.0}/src/licos_platform_cli/knowledge_commands.py +68 -45
  7. {licos_platform_cli-0.5.1 → licos_platform_cli-0.6.0}/src/licos_platform_cli/ontology_commands.py +6 -3
  8. {licos_platform_cli-0.5.1 → licos_platform_cli-0.6.0}/src/licos_platform_cli/user_commands.py +19 -7
  9. {licos_platform_cli-0.5.1 → licos_platform_cli-0.6.0}/tests/test_cli.py +17 -6
  10. licos_platform_cli-0.6.0/tests/test_identity.py +31 -0
  11. {licos_platform_cli-0.5.1 → licos_platform_cli-0.6.0}/tests/test_user.py +7 -3
  12. {licos_platform_cli-0.5.1 → licos_platform_cli-0.6.0}/.gitignore +0 -0
  13. {licos_platform_cli-0.5.1 → licos_platform_cli-0.6.0}/src/licos_platform_cli/__init__.py +0 -0
  14. {licos_platform_cli-0.5.1 → licos_platform_cli-0.6.0}/src/licos_platform_cli/database_commands.py +0 -0
  15. {licos_platform_cli-0.5.1 → licos_platform_cli-0.6.0}/src/licos_platform_cli/main.py +0 -0
  16. {licos_platform_cli-0.5.1 → licos_platform_cli-0.6.0}/src/licos_platform_cli/oauth_commands.py +0 -0
  17. {licos_platform_cli-0.5.1 → licos_platform_cli-0.6.0}/src/licos_platform_cli/project_config_commands.py +0 -0
  18. {licos_platform_cli-0.5.1 → licos_platform_cli-0.6.0}/src/licos_platform_cli/storage_commands.py +0 -0
  19. {licos_platform_cli-0.5.1 → licos_platform_cli-0.6.0}/tests/test_project_config.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: licos-platform-cli
3
- Version: 0.5.1
3
+ Version: 0.6.0
4
4
  Summary: LICOS platform CLI
5
5
  Requires-Python: >=3.10
6
- Requires-Dist: licos-platform-sdk>=0.6.1
6
+ Requires-Dist: licos-platform-sdk>=0.7.0
7
7
  Description-Content-Type: text/markdown
8
8
 
9
9
  # licos-platform-cli
@@ -18,7 +18,7 @@ licos-platform user me --workspace-id <执行工作空间ID>
18
18
  licos-platform help user me
19
19
  ```
20
20
 
21
- 默认由可信运行时注入 `LICOS_CURRENT_USER_ID`,平台地址、服务凭证和项目工作空间沿用运行时配置。
21
+ 用户接口默认使用 `LICOS_CURRENT_USER_ID` 指定的容器执行用户换取 Token。它不代表网页登录者。平台地址、服务凭证和项目工作空间沿用运行时配置。
22
22
  多用户请求可将当前使用者 Token 通过标准输入传入,不将 Token 放在命令行参数中:
23
23
 
24
24
  ```bash
@@ -31,3 +31,10 @@ licos-platform user me --token-stdin
31
31
  主部门、全部所属部门、组织任职、主岗位和主职务。部门含名称路径、ID 路径;
32
32
  无部门/任职时保持 `[]`/`null`。缺少当前使用者身份或 Token 失效时返回非零退出码,
33
33
  不改用项目所有者。Token 和内部服务凭证不回显。
34
+
35
+
36
+ ## 身份选择(0.6.0 起)
37
+
38
+ `user me`、`knowledge enterprise …`、`ontology …` 均支持 `--token-stdin`。HTTP 服务调用这些命令时必须从标准输入传本次网页登录 Token;独立 CLI 不传此选项时按 `LICOS_CURRENT_USER_ID` 换 Token,缺少该变量则报错。
39
+
40
+ 普通 `knowledge …` 命令默认使用项目身份,`--token-stdin` 可显式按调用用户授权。数据库、存储和 OAuth 应用管理仍使用项目身份。空 Token、失效 Token 不切换身份。完整接口分类见仓库 `tools/IDENTITY.md`。
@@ -10,7 +10,7 @@ licos-platform user me --workspace-id <执行工作空间ID>
10
10
  licos-platform help user me
11
11
  ```
12
12
 
13
- 默认由可信运行时注入 `LICOS_CURRENT_USER_ID`,平台地址、服务凭证和项目工作空间沿用运行时配置。
13
+ 用户接口默认使用 `LICOS_CURRENT_USER_ID` 指定的容器执行用户换取 Token。它不代表网页登录者。平台地址、服务凭证和项目工作空间沿用运行时配置。
14
14
  多用户请求可将当前使用者 Token 通过标准输入传入,不将 Token 放在命令行参数中:
15
15
 
16
16
  ```bash
@@ -23,3 +23,10 @@ licos-platform user me --token-stdin
23
23
  主部门、全部所属部门、组织任职、主岗位和主职务。部门含名称路径、ID 路径;
24
24
  无部门/任职时保持 `[]`/`null`。缺少当前使用者身份或 Token 失效时返回非零退出码,
25
25
  不改用项目所有者。Token 和内部服务凭证不回显。
26
+
27
+
28
+ ## 身份选择(0.6.0 起)
29
+
30
+ `user me`、`knowledge enterprise …`、`ontology …` 均支持 `--token-stdin`。HTTP 服务调用这些命令时必须从标准输入传本次网页登录 Token;独立 CLI 不传此选项时按 `LICOS_CURRENT_USER_ID` 换 Token,缺少该变量则报错。
31
+
32
+ 普通 `knowledge …` 命令默认使用项目身份,`--token-stdin` 可显式按调用用户授权。数据库、存储和 OAuth 应用管理仍使用项目身份。空 Token、失效 Token 不切换身份。完整接口分类见仓库 `tools/IDENTITY.md`。
@@ -4,12 +4,12 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "licos-platform-cli"
7
- version = "0.5.1"
7
+ version = "0.6.0"
8
8
  description = "LICOS platform CLI"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  dependencies = [
12
- "licos-platform-sdk>=0.6.1",
12
+ "licos-platform-sdk>=0.7.0",
13
13
  ]
14
14
 
15
15
  [project.scripts]
@@ -379,7 +379,7 @@ KNOWLEDGE_HELP = {
379
379
  "Do not manage graph/ACL internals from generated application code.",
380
380
  "Search all accessible datasets by default; pass dataset IDs or names only when the user explicitly asks for specific datasets.",
381
381
  "For text imports, the SDK sends UTF-8 text as a text/plain base64 document through the platform API.",
382
- "Enterprise commands require an enterprise user. Tenant scope and permissions come from the current user token; never pass or invent tenant IDs.",
382
+ "Enterprise commands use --token-stdin or the standalone CLI user LICOS_CURRENT_USER_ID. Tenant scope and permissions come from the current user token; never pass or invent tenant IDs.",
383
383
  "Enterprise tree supports STANDARD, GRAPH, and OBJECT_STORAGE type filters. Current enterprise APIs support retrieve and list_files only.",
384
384
  "Validate enterprise knowledge references before retrieval or listing when an Agent receives IDs from conversation context. Manage enterprise resources through the platform knowledge IFrame.",
385
385
  "Use document-url/document-download-link for temporary access URLs; do not construct storage URLs.",
@@ -387,8 +387,8 @@ KNOWLEDGE_HELP = {
387
387
  "sdk": {
388
388
  "python": "from licos_platform_sdk import knowledge\nknowledge.add_text('FAQ content', dataset_name='project_docs')\nresults = knowledge.search('How to reset password?')",
389
389
  "node": "import { knowledge } from '@kmlckj/licos-platform-sdk';\nawait knowledge.addText('FAQ content', { datasetName: 'project_docs' });\nconst results = await knowledge.search('How to reset password?');",
390
- "enterprise_python": "from licos_platform_sdk import enterprise_knowledge\nresources = enterprise_knowledge.tree()\nresult = enterprise_knowledge.retrieve('安全规范', 'source-id')",
391
- "enterprise_node": "import { enterpriseKnowledge } from '@kmlckj/licos-platform-sdk';\nconst resources = await enterpriseKnowledge.tree();\nconst result = await enterpriseKnowledge.retrieve('安全规范', 'source-id');",
390
+ "enterprise_python": "from licos_platform_sdk import enterprise_knowledge\nresources = enterprise_knowledge.tree(user_token=token)\nresult = enterprise_knowledge.retrieve('安全规范', 'source-id', user_token=token)",
391
+ "enterprise_node": "import { enterpriseKnowledge } from '@kmlckj/licos-platform-sdk';\nconst resources = await enterpriseKnowledge.tree({ userToken: token });\nconst result = await enterpriseKnowledge.retrieve('安全规范', 'source-id', { userToken: token });",
392
392
  },
393
393
  "commands": {'runtime': {'description': 'Show knowledge runtime status and capabilities.',
394
394
  'parameters': [],
@@ -798,27 +798,28 @@ ONTOLOGY_HELP = {
798
798
  "Resolve elements by stable IDs before querying context, model checks, or mapped data.",
799
799
  "READ tools query state, PREPARE tools validate a change draft, and PERSIST tools apply an explicitly confirmed draft.",
800
800
  "PERSIST commands require --confirm. Never reuse a catalog result after its catalogVersion changes.",
801
- "实时订阅由项目后端 SDK 完成,CLI 不提供常驻 WebSocket 命令;不得读取 Token 或自行拼上游连接。",
801
+ "实时订阅由项目后端 SDK 完成,必须传入本次认证会话 Token;CLI 不提供常驻 WebSocket 命令。",
802
802
  ],
803
803
  "realtimeSdk": {
804
- "node": "const subscription = await ontology.subscribeRealtime({ elementIds, attributeIds });\ntry { for await (const event of subscription) { sendBusinessEvent(event); } }\nfinally { await subscription.close(); }",
805
- "python": "with ontology.subscribe_realtime(element_ids, attribute_ids=attribute_ids) as subscription:\n for event in subscription:\n send_business_event(event)",
806
- "minimum_versions": {"node": "0.10.5", "python": "0.5.5"},
804
+ "node": "const subscription = await ontology.subscribeRealtime({ elementIds, attributeIds }, { userToken: token });\ntry { for await (const event of subscription) { sendBusinessEvent(event); } }\nfinally { await subscription.close(); }",
805
+ "python": "with ontology.subscribe_realtime(element_ids, attribute_ids=attribute_ids, user_token=token) as subscription:\n for event in subscription:\n send_business_event(event)",
806
+ "minimum_versions": {"node": "0.12.0", "python": "0.7.0"},
807
807
  "parameters": {
808
+ "userToken / user_token": "必填,本次认证会话的用户 Token,不含 Bearer 前缀。",
808
809
  "elementIds / element_ids": "必填,非空字符串 ID 数组;已解析且授权的元素,不递归子元素。",
809
810
  "attributeIds / attribute_ids": "可选,字符串 ID 数组;省略或空数组表示所选元素的全部授权属性。",
810
811
  },
811
812
  "returns": "收到订阅确认后返回订阅对象;单消费者迭代 property.changed 原始业务事件,close 取消订阅。Node 使用异步迭代,Python 使用同步 with/for,异步服务放入工作线程。",
812
- "errors": "创建错误直接抛出,后续错误从迭代器抛出;401 仅刷新一次,403/协议错误不重试,网络重连累计默认 3 次。不得把获取连接计划当成订阅成功。",
813
+ "errors": "创建错误直接抛出,后续错误从迭代器抛出;401/403 终止,由调用方更新 Token;网络中断默认使用原 Token 持续重连并重新检查权限。不得把获取连接计划当成订阅成功。",
813
814
  "reference": "industrial-ontology/references/realtime-sdk.md",
814
815
  },
815
816
  "sdk": {
816
- "python": "from licos_platform_sdk import ontology\ncatalog = ontology.search_tools('查询设备实时数据')\nselected = next((item for item in catalog['items'] if item['selection'] == 'MATCH' and item['mode'] == 'READ' and item['definition']['function']['name'] == requested_tool_name), None)\nif selected is None:\n raise RuntimeError('当前目录中没有匹配的只读本体工具')\n# 按 selected['definition']['function']['parameters'] 校验参数后调用精确函数名。",
817
- "node": "import { ontology } from '@kmlckj/licos-platform-sdk';\nconst catalog = await ontology.searchTools('查询设备实时数据');\nconst selected = catalog.items.find((item) => item.selection === 'MATCH' && item.mode === 'READ' && item.definition.function.name === requestedToolName);\nif (!selected) throw new Error('当前目录中没有匹配的只读本体工具');\n// 按 selected.definition.function.parameters 校验参数后调用精确函数名。",
817
+ "python": "from licos_platform_sdk import ontology\ncatalog = ontology.search_tools('查询设备实时数据', user_token=token)\nselected = next((item for item in catalog['items'] if item['selection'] == 'MATCH' and item['mode'] == 'READ' and item['definition']['function']['name'] == requested_tool_name), None)\nif selected is None:\n raise RuntimeError('当前目录中没有匹配的只读本体工具')\n# 按 selected['definition']['function']['parameters'] 校验参数后调用精确函数名。",
818
+ "node": "import { ontology } from '@kmlckj/licos-platform-sdk';\nconst catalog = await ontology.searchTools('查询设备实时数据', { userToken: token });\nconst selected = catalog.items.find((item) => item.selection === 'MATCH' && item.mode === 'READ' && item.definition.function.name === requestedToolName);\nif (!selected) throw new Error('当前目录中没有匹配的只读本体工具');\n// 按 selected.definition.function.parameters 校验参数后调用精确函数名。",
818
819
  },
819
820
  "commands": {
820
821
  "search": {
821
- "description": "Search the current user's ontology AI tool catalog.",
822
+ "description": "Search the user catalog; use --token-stdin for a web request or LICOS_CURRENT_USER_ID for standalone CLI.",
822
823
  "parameters": [
823
824
  {"name": "query", "required": True, "description": "Natural-language capability query."},
824
825
  {"name": "--limit", "required": False, "description": "Result count from 1 to 10; default 5."},
@@ -850,6 +851,16 @@ HELP_TOPICS = {
850
851
  "ontology": ONTOLOGY_HELP,
851
852
  }
852
853
 
854
+ for topic in (KNOWLEDGE_HELP, ONTOLOGY_HELP):
855
+ for command_name, command_help in topic["commands"].items():
856
+ user_scoped = topic is ONTOLOGY_HELP or command_name.startswith("enterprise-")
857
+ command_help["parameters"].append({
858
+ "name": "--token-stdin", "required": False,
859
+ "description": "Read this request's user Token from stdin; otherwise " + (
860
+ "exchange LICOS_CURRENT_USER_ID for standalone CLI." if user_scoped else "use project identity."
861
+ ),
862
+ })
863
+
853
864
 
854
865
  def _set_handler(parser: argparse.ArgumentParser, handler: Handler) -> None:
855
866
  parser.set_defaults(handler=handler)
@@ -0,0 +1,26 @@
1
+ """Select the CLI execution identity without changing SDK or process state."""
2
+
3
+ import os
4
+ import sys
5
+
6
+ from licos_platform_sdk import ConfigurationError, runtime_user_token
7
+
8
+
9
+ def user_token(args) -> str:
10
+ if args.token_stdin:
11
+ token = sys.stdin.read().strip()
12
+ if not token:
13
+ raise ConfigurationError("--token-stdin requires a nonempty user Token")
14
+ return token
15
+ user_id = os.environ.get("LICOS_CURRENT_USER_ID", "").strip()
16
+ if not user_id:
17
+ raise ConfigurationError("CLI user identity requires LICOS_CURRENT_USER_ID or --token-stdin")
18
+ return runtime_user_token(user_id)
19
+
20
+
21
+ def add_user_token_option(parser) -> None:
22
+ parser.add_argument(
23
+ "--token-stdin",
24
+ action="store_true",
25
+ help="Read the caller Token from stdin; otherwise use LICOS_CURRENT_USER_ID",
26
+ )
@@ -4,15 +4,27 @@ import argparse
4
4
  from typing import Any, Callable
5
5
 
6
6
  from licos_platform_sdk import enterprise_knowledge, knowledge
7
-
7
+ from .identity import add_user_token_option, user_token
8
8
 
9
9
  Handler = Callable[[argparse.Namespace], Any]
10
10
 
11
11
 
12
12
  def _set_handler(parser: argparse.ArgumentParser, handler: Handler) -> None:
13
+ parser.add_argument(
14
+ "--token-stdin", action="store_true", help="Use the request Token from stdin; omission uses project identity"
15
+ )
13
16
  parser.set_defaults(handler=handler)
14
17
 
15
18
 
19
+ def _set_user_handler(parser, handler):
20
+ add_user_token_option(parser)
21
+ parser.set_defaults(handler=handler)
22
+
23
+
24
+ def _project_identity(args):
25
+ return {"user_token": user_token(args)} if args.token_stdin else {}
26
+
27
+
16
28
  def _chunk_config(args: argparse.Namespace) -> knowledge.ChunkConfig | None:
17
29
  if not any(
18
30
  [
@@ -35,12 +47,12 @@ def _dataset_selector(args: argparse.Namespace) -> dict[str, str | None]:
35
47
  return {"dataset_id": args.dataset_id, "dataset_name": args.dataset}
36
48
 
37
49
 
38
- def _cmd_runtime(_args: argparse.Namespace) -> Any:
39
- return knowledge.runtime()
50
+ def _cmd_runtime(args: argparse.Namespace) -> Any:
51
+ return knowledge.runtime(**_project_identity(args))
40
52
 
41
53
 
42
- def _cmd_model_catalog(_args: argparse.Namespace) -> Any:
43
- return knowledge.model_catalog()
54
+ def _cmd_model_catalog(args: argparse.Namespace) -> Any:
55
+ return knowledge.model_catalog(**_project_identity(args))
44
56
 
45
57
 
46
58
  def _cmd_datasets(args: argparse.Namespace) -> Any:
@@ -50,6 +62,7 @@ def _cmd_datasets(args: argparse.Namespace) -> Any:
50
62
  page_size=args.page_size,
51
63
  page_num=args.page_num,
52
64
  format_type=args.format_type,
65
+ **_project_identity(args),
53
66
  )
54
67
 
55
68
 
@@ -62,28 +75,39 @@ def _cmd_create_dataset(args: argparse.Namespace) -> Any:
62
75
  source_type=args.source_type,
63
76
  engine_type=args.engine_type,
64
77
  access_scope=args.access_scope,
78
+ **_project_identity(args),
65
79
  )
66
80
 
67
81
 
68
82
  def _cmd_ensure_dataset(args: argparse.Namespace) -> Any:
69
- return knowledge.ensure_dataset(args.name, description=args.description, space_id=args.space_id)
83
+ return knowledge.ensure_dataset(
84
+ args.name, description=args.description, space_id=args.space_id, **_project_identity(args)
85
+ )
70
86
 
71
87
 
72
88
  def _cmd_get_dataset(args: argparse.Namespace) -> Any:
73
- return knowledge.get_dataset(args.dataset_id)
89
+ return knowledge.get_dataset(args.dataset_id, **_project_identity(args))
74
90
 
75
91
 
76
92
  def _cmd_delete_dataset(args: argparse.Namespace) -> Any:
77
- return knowledge.delete_dataset(args.dataset_id)
93
+ return knowledge.delete_dataset(args.dataset_id, **_project_identity(args))
78
94
 
79
95
 
80
96
  def _cmd_add(args: argparse.Namespace) -> Any:
81
97
  selector = _dataset_selector(args)
82
98
  chunk_config = _chunk_config(args)
83
99
  if args.content is not None:
84
- return knowledge.add_text(args.content, name=args.name or "document.txt", chunk_config=chunk_config, **selector)
100
+ return knowledge.add_text(
101
+ args.content,
102
+ name=args.name or "document.txt",
103
+ chunk_config=chunk_config,
104
+ **selector,
105
+ **_project_identity(args),
106
+ )
85
107
  if args.url is not None:
86
- return knowledge.add_url(args.url, name=args.name, chunk_config=chunk_config, **selector)
108
+ return knowledge.add_url(
109
+ args.url, name=args.name, chunk_config=chunk_config, **selector, **_project_identity(args)
110
+ )
87
111
  return knowledge.add_documents(
88
112
  [
89
113
  knowledge.KnowledgeDocument(
@@ -95,6 +119,7 @@ def _cmd_add(args: argparse.Namespace) -> Any:
95
119
  ],
96
120
  chunk_config=chunk_config,
97
121
  **selector,
122
+ **_project_identity(args),
98
123
  )
99
124
 
100
125
 
@@ -110,38 +135,36 @@ def _cmd_search(args: argparse.Namespace) -> Any:
110
135
  candidate_top_k=args.candidate_top_k,
111
136
  search_method=args.search_method,
112
137
  enable_rerank=args.enable_rerank,
138
+ **_project_identity(args),
113
139
  )
114
140
 
115
141
 
116
142
  def _cmd_documents(args: argparse.Namespace) -> Any:
117
- return knowledge.list_documents(args.dataset_id, page=args.page, size=args.size)
143
+ return knowledge.list_documents(args.dataset_id, page=args.page, size=args.size, **_project_identity(args))
118
144
 
119
145
 
120
146
  def _cmd_delete_documents(args: argparse.Namespace) -> Any:
121
- return knowledge.delete_documents(args.dataset_id, args.document_id)
147
+ return knowledge.delete_documents(args.dataset_id, args.document_id, **_project_identity(args))
122
148
 
123
149
 
124
150
  def _cmd_enterprise_tree(args: argparse.Namespace) -> Any:
125
- return enterprise_knowledge.tree(include_types=args.include_types)
151
+ return enterprise_knowledge.tree(include_types=args.include_types, user_token=user_token(args))
126
152
 
127
153
 
128
154
  def _cmd_enterprise_search(args: argparse.Namespace) -> Any:
129
155
  return enterprise_knowledge.search_knowledge_bases(
130
- args.query,
131
- page=args.page,
132
- page_size=args.page_size,
156
+ args.query, page=args.page, page_size=args.page_size, user_token=user_token(args)
133
157
  )
134
158
 
135
159
 
136
160
  def _cmd_enterprise_validate(args: argparse.Namespace) -> Any:
137
161
  return enterprise_knowledge.validate_references(
138
- args.knowledge_base_id,
139
- required_capability=args.required_capability,
162
+ args.knowledge_base_id, required_capability=args.required_capability, user_token=user_token(args)
140
163
  )
141
164
 
142
165
 
143
166
  def _cmd_enterprise_retrieve(args: argparse.Namespace) -> Any:
144
- return enterprise_knowledge.retrieve(args.query, args.source_id)
167
+ return enterprise_knowledge.retrieve(args.query, args.source_id, user_token=user_token(args))
145
168
 
146
169
 
147
170
  def _cmd_enterprise_folders(args: argparse.Namespace) -> Any:
@@ -150,11 +173,12 @@ def _cmd_enterprise_folders(args: argparse.Namespace) -> Any:
150
173
  include_children=args.include_children,
151
174
  page=args.page,
152
175
  page_size=args.page_size,
176
+ user_token=user_token(args),
153
177
  )
154
178
 
155
179
 
156
180
  def _cmd_enterprise_folder(args: argparse.Namespace) -> Any:
157
- return enterprise_knowledge.get_folder(args.folder_id)
181
+ return enterprise_knowledge.get_folder(args.folder_id, user_token=user_token(args))
158
182
 
159
183
 
160
184
  def _cmd_enterprise_folder_knowledge_bases(args: argparse.Namespace) -> Any:
@@ -163,40 +187,40 @@ def _cmd_enterprise_folder_knowledge_bases(args: argparse.Namespace) -> Any:
163
187
  include_children=args.include_children,
164
188
  page=args.page,
165
189
  page_size=args.page_size,
190
+ user_token=user_token(args),
166
191
  )
167
192
 
168
193
 
169
194
  def _cmd_enterprise_knowledge_base(args: argparse.Namespace) -> Any:
170
- return enterprise_knowledge.get_knowledge_base(args.source_id)
195
+ return enterprise_knowledge.get_knowledge_base(args.source_id, user_token=user_token(args))
171
196
 
172
197
 
173
198
  def _cmd_enterprise_documents(args: argparse.Namespace) -> Any:
174
199
  return enterprise_knowledge.list_documents(
175
- args.source_id,
176
- page=args.page,
177
- page_size=args.page_size,
178
- query=args.query,
200
+ args.source_id, page=args.page, page_size=args.page_size, query=args.query, user_token=user_token(args)
179
201
  )
180
202
 
181
203
 
182
204
  def _cmd_enterprise_document(args: argparse.Namespace) -> Any:
183
- return enterprise_knowledge.get_document(args.document_id)
205
+ return enterprise_knowledge.get_document(args.document_id, user_token=user_token(args))
184
206
 
185
207
 
186
208
  def _cmd_enterprise_document_chunks(args: argparse.Namespace) -> Any:
187
209
  return enterprise_knowledge.list_document_chunks(
188
- args.document_id,
189
- page=args.page,
190
- page_size=args.page_size,
210
+ args.document_id, page=args.page, page_size=args.page_size, user_token=user_token(args)
191
211
  )
192
212
 
193
213
 
194
214
  def _cmd_enterprise_document_url(args: argparse.Namespace) -> Any:
195
- return {"url": enterprise_knowledge.document_original_url(args.document_id, download=args.download)}
215
+ return {
216
+ "url": enterprise_knowledge.document_original_url(
217
+ args.document_id, download=args.download, user_token=user_token(args)
218
+ )
219
+ }
196
220
 
197
221
 
198
222
  def _cmd_enterprise_document_download_link(args: argparse.Namespace) -> Any:
199
- return enterprise_knowledge.document_download_link(args.document_id)
223
+ return enterprise_knowledge.document_download_link(args.document_id, user_token=user_token(args))
200
224
 
201
225
 
202
226
  def _add_dataset_selector(parser: argparse.ArgumentParser) -> None:
@@ -244,12 +268,12 @@ def _add_enterprise_parser(
244
268
 
245
269
  tree = commands.add_parser("tree", help="Get the accessible enterprise knowledge resource tree")
246
270
  _add_enterprise_types(tree)
247
- _set_handler(tree, _cmd_enterprise_tree)
271
+ _set_user_handler(tree, _cmd_enterprise_tree)
248
272
 
249
273
  search = commands.add_parser("search", help="Search enterprise knowledge bases by name")
250
274
  search.add_argument("--query", "-q", required=True)
251
275
  _add_page_args(search)
252
- _set_handler(search, _cmd_enterprise_search)
276
+ _set_user_handler(search, _cmd_enterprise_search)
253
277
 
254
278
  validate = commands.add_parser("validate", help="Validate knowledge base references and capabilities")
255
279
  validate.add_argument("knowledge_base_id", nargs="+", help="Knowledge base ID")
@@ -258,22 +282,22 @@ def _add_enterprise_parser(
258
282
  choices=["retrieve", "upload", "list_files", "batch_delete_files"],
259
283
  default="retrieve",
260
284
  )
261
- _set_handler(validate, _cmd_enterprise_validate)
285
+ _set_user_handler(validate, _cmd_enterprise_validate)
262
286
 
263
287
  retrieve = commands.add_parser("retrieve", help="Retrieve evidence from enterprise knowledge bases")
264
288
  retrieve.add_argument("--query", "-q", required=True)
265
289
  retrieve.add_argument("--source-id", required=True, help="Knowledge base source ID")
266
- _set_handler(retrieve, _cmd_enterprise_retrieve)
290
+ _set_user_handler(retrieve, _cmd_enterprise_retrieve)
267
291
 
268
292
  folders = commands.add_parser("folders", help="List enterprise knowledge folders")
269
293
  folders.add_argument("--parent-id", default=None)
270
294
  folders.add_argument("--include-children", action="store_true", default=None)
271
295
  _add_page_args(folders)
272
- _set_handler(folders, _cmd_enterprise_folders)
296
+ _set_user_handler(folders, _cmd_enterprise_folders)
273
297
 
274
298
  folder = commands.add_parser("folder", help="Get one enterprise knowledge folder")
275
299
  folder.add_argument("folder_id")
276
- _set_handler(folder, _cmd_enterprise_folder)
300
+ _set_user_handler(folder, _cmd_enterprise_folder)
277
301
 
278
302
  folder_bases = commands.add_parser(
279
303
  "folder-knowledge-bases",
@@ -282,35 +306,34 @@ def _add_enterprise_parser(
282
306
  folder_bases.add_argument("folder_id")
283
307
  folder_bases.add_argument("--include-children", action="store_true", default=None)
284
308
  _add_page_args(folder_bases)
285
- _set_handler(folder_bases, _cmd_enterprise_folder_knowledge_bases)
309
+ _set_user_handler(folder_bases, _cmd_enterprise_folder_knowledge_bases)
286
310
 
287
311
  knowledge_base = commands.add_parser("knowledge-base", help="Get one enterprise knowledge base")
288
312
  knowledge_base.add_argument("source_id")
289
- _set_handler(knowledge_base, _cmd_enterprise_knowledge_base)
313
+ _set_user_handler(knowledge_base, _cmd_enterprise_knowledge_base)
290
314
 
291
315
  documents = commands.add_parser("documents", help="List documents in an enterprise knowledge base")
292
316
  documents.add_argument("source_id")
293
317
  _add_enterprise_query_args(documents)
294
- _set_handler(documents, _cmd_enterprise_documents)
318
+ _set_user_handler(documents, _cmd_enterprise_documents)
295
319
 
296
320
  document = commands.add_parser("document", help="Get one enterprise knowledge document")
297
321
  document.add_argument("document_id")
298
- _set_handler(document, _cmd_enterprise_document)
322
+ _set_user_handler(document, _cmd_enterprise_document)
299
323
 
300
324
  chunks = commands.add_parser("document-chunks", help="List chunks for one enterprise knowledge document")
301
325
  chunks.add_argument("document_id")
302
326
  _add_page_args(chunks)
303
- _set_handler(chunks, _cmd_enterprise_document_chunks)
327
+ _set_user_handler(chunks, _cmd_enterprise_document_chunks)
304
328
 
305
329
  document_url = commands.add_parser("document-url", help="Get a temporary original document URL")
306
330
  document_url.add_argument("document_id")
307
331
  document_url.add_argument("--download", action="store_true", help="Request download disposition")
308
- _set_handler(document_url, _cmd_enterprise_document_url)
332
+ _set_user_handler(document_url, _cmd_enterprise_document_url)
309
333
 
310
334
  download_link = commands.add_parser("document-download-link", help="Get a temporary document download link")
311
335
  download_link.add_argument("document_id")
312
- _set_handler(download_link, _cmd_enterprise_document_download_link)
313
-
336
+ _set_user_handler(download_link, _cmd_enterprise_document_download_link)
314
337
 
315
338
 
316
339
  def add_knowledge_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
@@ -6,12 +6,13 @@ from pathlib import Path
6
6
  from typing import Any, Callable
7
7
 
8
8
  from licos_platform_sdk import ontology
9
-
9
+ from .identity import add_user_token_option, user_token
10
10
 
11
11
  Handler = Callable[[argparse.Namespace], Any]
12
12
 
13
13
 
14
14
  def _set_handler(parser: argparse.ArgumentParser, handler: Handler) -> None:
15
+ add_user_token_option(parser)
15
16
  parser.set_defaults(handler=handler)
16
17
 
17
18
 
@@ -29,11 +30,12 @@ def _arguments(args: argparse.Namespace) -> dict[str, Any]:
29
30
 
30
31
 
31
32
  def _cmd_search(args: argparse.Namespace) -> Any:
32
- return ontology.search_tools(args.query, limit=args.limit, offset=args.offset)
33
+ return ontology.search_tools(args.query, limit=args.limit, offset=args.offset, user_token=user_token(args))
33
34
 
34
35
 
35
36
  def _cmd_invoke(args: argparse.Namespace) -> Any:
36
- catalog_version, item = ontology.resolve_tool(args.name)
37
+ token = user_token(args)
38
+ catalog_version, item = ontology.resolve_tool(args.name, user_token=token)
37
39
  if args.catalog_version and args.catalog_version != catalog_version:
38
40
  raise argparse.ArgumentTypeError(
39
41
  "ontology tool catalog changed; search again before invoking the tool"
@@ -46,6 +48,7 @@ def _cmd_invoke(args: argparse.Namespace) -> Any:
46
48
  args.name,
47
49
  _arguments(args),
48
50
  catalog_version=catalog_version,
51
+ user_token=token,
49
52
  )
50
53
 
51
54
 
@@ -1,10 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
- import sys
5
4
 
6
5
  from licos_platform_sdk import get_current_user
7
-
6
+ from .identity import user_token
8
7
 
9
8
  USER_HELP = {
10
9
  "topic": "user",
@@ -12,8 +11,11 @@ USER_HELP = {
12
11
  "runtime": [
13
12
  {"name": "LICOS_PLATFORM_API_BASE_URL", "description": "平台 API 地址。"},
14
13
  {"name": "LICOS_AI_AGENT_TOKEN", "description": "运行时内部服务凭证,不输出。"},
15
- {"name": "LICOS_CURRENT_USER_ID", "description": "未通过标准输入传 Token 时,必须由可信运行时注入的当前用户 ID。"},
16
- {"name": "LICOS_WORKSPACE_ID / LICOS_OWNER_WORKSPACE_ID / AGENT_WORKSPACE_ID", "description": "执行工作空间,决定企业范围。"},
14
+ {"name": "LICOS_CURRENT_USER_ID", "description": "独立 CLI 默认执行用户,即容器初始化用户;不代表网页登录者。"},
15
+ {
16
+ "name": "LICOS_WORKSPACE_ID / LICOS_OWNER_WORKSPACE_ID / AGENT_WORKSPACE_ID",
17
+ "description": "执行工作空间,决定企业范围。",
18
+ },
17
19
  ],
18
20
  "rules": [
19
21
  "多用户请求传当前使用者 Token;不使用项目所有者代替使用者。",
@@ -28,8 +30,18 @@ USER_HELP = {
28
30
  "me": {
29
31
  "description": "查询当前使用者信息。",
30
32
  "parameters": [
31
- {"name": "--token-stdin", "type": "boolean", "required": False, "description": "从标准输入读取当前用户 Token,不含 Bearer 前缀;不通过命令行参数暴露 Token。"},
32
- {"name": "--workspace-id", "type": "string", "required": False, "description": "执行工作空间 ID;省略时由项目运行时获取。"},
33
+ {
34
+ "name": "--token-stdin",
35
+ "type": "boolean",
36
+ "required": False,
37
+ "description": "从标准输入读取当前用户 Token,不含 Bearer 前缀;不通过命令行参数暴露 Token。",
38
+ },
39
+ {
40
+ "name": "--workspace-id",
41
+ "type": "string",
42
+ "required": False,
43
+ "description": "执行工作空间 ID;省略时由项目运行时获取。",
44
+ },
33
45
  ],
34
46
  "returns": "sys_user 对象:id、username、displayName、avatarUrl、tenantId、tenantName、primaryDepartment、departments、organizationAssignments、primaryPosition、primaryJobTitle。",
35
47
  "example": "licos-platform user me",
@@ -40,7 +52,7 @@ USER_HELP = {
40
52
 
41
53
  def _me(args: argparse.Namespace):
42
54
  return get_current_user(
43
- user_token=sys.stdin.read().strip() if args.token_stdin else None,
55
+ user_token=user_token(args),
44
56
  workspace_id=args.workspace_id,
45
57
  )
46
58
 
@@ -36,6 +36,15 @@ def run_cli(*args: str) -> tuple[int, str, str]:
36
36
 
37
37
 
38
38
  class PlatformCliTests(unittest.TestCase):
39
+
40
+ def setUp(self):
41
+ identity = mock.patch("licos_platform_cli.identity.runtime_user_token", return_value="cli-token")
42
+ identity.start()
43
+ self.addCleanup(identity.stop)
44
+ env = mock.patch.dict(os.environ, {"LICOS_CURRENT_USER_ID": "cli-user"})
45
+ env.start()
46
+ self.addCleanup(env.stop)
47
+
39
48
  def test_enterprise_help_matches_removed_management_commands(self) -> None:
40
49
  code, output, _ = run_cli("help", "knowledge")
41
50
  self.assertEqual(code, 0)
@@ -53,7 +62,7 @@ class PlatformCliTests(unittest.TestCase):
53
62
  code, output, _ = run_cli("knowledge", "enterprise", "document-download-link", "doc-1")
54
63
  self.assertEqual(code, 0)
55
64
  self.assertEqual(json.loads(output), link)
56
- download.assert_called_once_with("doc-1")
65
+ download.assert_called_once_with("doc-1", user_token="cli-token")
57
66
 
58
67
  def test_help_exposes_storage_commands(self) -> None:
59
68
  stdout = io.StringIO()
@@ -136,7 +145,10 @@ class PlatformCliTests(unittest.TestCase):
136
145
  self.assertIn("mode", sdk_help)
137
146
  self.assertIn("subscribeRealtime", payload["realtimeSdk"]["node"])
138
147
  self.assertIn("subscribe_realtime", payload["realtimeSdk"]["python"])
139
- self.assertEqual(payload["realtimeSdk"]["minimum_versions"], {"node": "0.10.5", "python": "0.5.5"})
148
+ self.assertEqual(payload["realtimeSdk"]["minimum_versions"], {"node": "0.12.0", "python": "0.7.0"})
149
+ self.assertIn("userToken", payload["realtimeSdk"]["node"])
150
+ self.assertIn("user_token", payload["realtimeSdk"]["python"])
151
+ self.assertIn("--token-stdin", [item["name"] for item in payload["commands"]["search"]["parameters"]])
140
152
 
141
153
  def test_ontology_search_forwards_catalog_pagination(self) -> None:
142
154
  result = {"catalogVersion": "v1", "items": []}
@@ -150,7 +162,7 @@ class PlatformCliTests(unittest.TestCase):
150
162
  self.assertEqual(code, 0)
151
163
  self.assertEqual(stderr, "")
152
164
  self.assertEqual(json.loads(stdout), result)
153
- search.assert_called_once_with("查询设备数据", limit=7, offset=3)
165
+ search.assert_called_once_with("查询设备数据", limit=7, offset=3, user_token="cli-token")
154
166
 
155
167
  def test_ontology_persist_requires_confirmation(self) -> None:
156
168
  item = {"mode": "PERSIST"}
@@ -187,7 +199,7 @@ class PlatformCliTests(unittest.TestCase):
187
199
  self.assertEqual(stderr, "")
188
200
  self.assertEqual(json.loads(stdout), {"value": 7})
189
201
  invoke.assert_called_once_with(
190
- "queryElementData", {"elementId": "element-1"}, catalog_version="v4"
202
+ "queryElementData", {"elementId": "element-1"}, catalog_version="v4", user_token="cli-token"
191
203
  )
192
204
 
193
205
  def test_ontology_invoke_rejects_changed_catalog_version(self) -> None:
@@ -689,8 +701,7 @@ class PlatformCliTests(unittest.TestCase):
689
701
  self.assertEqual(code, 0)
690
702
  self.assertEqual(stderr, "")
691
703
  self.assertEqual(json.loads(stdout), {"status": "KNOWLEDGE_TREE_READY"})
692
- tree.assert_called_once_with(include_types=["STANDARD", "GRAPH"])
693
-
704
+ tree.assert_called_once_with(include_types=["STANDARD", "GRAPH"], user_token="cli-token")
694
705
 
695
706
  def test_structured_help_describes_enterprise_knowledge_contract(self) -> None:
696
707
  code, stdout, stderr = run_cli("help", "knowledge", "enterprise-documents")
@@ -0,0 +1,31 @@
1
+ import io
2
+ from argparse import Namespace
3
+ from unittest import TestCase, mock
4
+
5
+ from licos_platform_cli.identity import user_token
6
+ from licos_platform_sdk import ConfigurationError
7
+
8
+
9
+ class CliIdentityTests(TestCase):
10
+ def test_cli_defaults_to_initialization_user(self):
11
+ with mock.patch.dict(
12
+ "os.environ", {"LICOS_CURRENT_USER_ID": "initializer", "LICOS_USER_ID": "owner"}, clear=True
13
+ ), mock.patch("licos_platform_cli.identity.runtime_user_token", return_value="cli-token") as exchange:
14
+ self.assertEqual(user_token(Namespace(token_stdin=False)), "cli-token")
15
+ exchange.assert_called_once_with("initializer")
16
+
17
+ def test_stdin_token_does_not_exchange_container_identity(self):
18
+ with mock.patch("sys.stdin", io.StringIO("request-token\n")), mock.patch(
19
+ "licos_platform_cli.identity.runtime_user_token"
20
+ ) as exchange:
21
+ self.assertEqual(user_token(Namespace(token_stdin=True)), "request-token")
22
+ exchange.assert_not_called()
23
+
24
+ def test_missing_user_and_empty_stdin_do_not_use_owner(self):
25
+ with mock.patch.dict("os.environ", {"LICOS_USER_ID": "owner"}, clear=True), mock.patch(
26
+ "sys.stdin", io.StringIO(" ")
27
+ ), mock.patch("licos_platform_cli.identity.runtime_user_token") as exchange:
28
+ for token_stdin in (True, False):
29
+ with self.assertRaises(ConfigurationError):
30
+ user_token(Namespace(token_stdin=token_stdin))
31
+ exchange.assert_not_called()
@@ -21,12 +21,16 @@ class UserCliTests(unittest.TestCase):
21
21
 
22
22
  def test_me_outputs_sdk_json_with_chinese(self):
23
23
  with patch.object(user_commands, "get_current_user", return_value={"id": "caller", "displayName": "张三"}) as sdk:
24
- code, out, err = self.run_cli("user", "me")
24
+ with patch(
25
+ "licos_platform_cli.identity.runtime_user_token", return_value="cli-token"
26
+ ) as exchange, patch.dict(os.environ, {"LICOS_CURRENT_USER_ID": "cli-user"}):
27
+ code, out, err = self.run_cli("user", "me")
28
+ exchange.assert_called_once_with("cli-user")
25
29
  self.assertEqual(code, 0)
26
30
  self.assertEqual(err, "")
27
31
  self.assertEqual(json.loads(out), {"id": "caller", "displayName": "张三"})
28
32
  self.assertIn("张三", out)
29
- sdk.assert_called_once_with(user_token=None, workspace_id=None)
33
+ sdk.assert_called_once_with(user_token="cli-token", workspace_id=None)
30
34
 
31
35
  def test_stdin_token_is_forwarded_without_echo(self):
32
36
  with patch("sys.stdin", io.StringIO("request-secret\n")), patch.object(
@@ -45,7 +49,7 @@ class UserCliTests(unittest.TestCase):
45
49
  code, out, err = self.run_cli("user", "me", "--token-stdin")
46
50
  self.assertEqual(code, 1)
47
51
  self.assertEqual(out, "")
48
- self.assertIn("不能为空", err)
52
+ self.assertIn("nonempty", err)
49
53
 
50
54
  def test_structured_help_exposes_command_and_sdk(self):
51
55
  code, out, err = self.run_cli("help", "user")