instance-repo 1.4.1.dev0__tar.gz → 1.6.0.dev2__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 (60) hide show
  1. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/PKG-INFO +1 -1
  2. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/__init__.py +1 -1
  3. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/_site_defaults.py +8 -0
  4. instance_repo-1.6.0.dev2/instance_repo/clients/_rfc3339.py +41 -0
  5. instance_repo-1.6.0.dev2/instance_repo/clients/ap_tool.py +265 -0
  6. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/config.py +205 -1
  7. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/instances.py +100 -3
  8. instance_repo-1.6.0.dev2/instance_repo/clients/shared.py +996 -0
  9. instance_repo-1.6.0.dev2/instance_repo/clients/user_data_acl.py +306 -0
  10. instance_repo-1.6.0.dev2/instance_repo/clients/user_data_directories.py +203 -0
  11. instance_repo-1.6.0.dev2/instance_repo/clients/user_data_entries.py +325 -0
  12. instance_repo-1.6.0.dev2/instance_repo/clients/user_data_reads.py +990 -0
  13. instance_repo-1.6.0.dev2/instance_repo/clients/user_data_upload.py +896 -0
  14. instance_repo-1.6.0.dev2/instance_repo/clients/user_data_writes.py +1806 -0
  15. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/errors.py +44 -11
  16. instance_repo-1.6.0.dev2/instance_repo/repo.py +1437 -0
  17. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/retry.py +30 -0
  18. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/store/oss.py +69 -0
  19. instance_repo-1.6.0.dev2/instance_repo/transport.py +898 -0
  20. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo.egg-info/PKG-INFO +1 -1
  21. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo.egg-info/SOURCES.txt +9 -0
  22. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/pyproject.toml +1 -1
  23. instance_repo-1.4.1.dev0/instance_repo/repo.py +0 -831
  24. instance_repo-1.4.1.dev0/instance_repo/transport.py +0 -438
  25. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/README.md +0 -0
  26. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/_routing.py +0 -0
  27. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/artifacts.py +0 -0
  28. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/bootstrap.py +0 -0
  29. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/cache.py +0 -0
  30. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/cli.py +0 -0
  31. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/__init__.py +0 -0
  32. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/benchmarks.py +0 -0
  33. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/datasets.py +0 -0
  34. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/images.py +0 -0
  35. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/reports.py +0 -0
  36. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/scaffold.py +0 -0
  37. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/splits.py +0 -0
  38. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/versions.py +0 -0
  39. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/clients/workflows.py +0 -0
  40. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/concurrency.py +0 -0
  41. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/content.py +0 -0
  42. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/endpoints.py +0 -0
  43. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/image.py +0 -0
  44. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/layout.py +0 -0
  45. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/loader.py +0 -0
  46. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/models.py +0 -0
  47. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/paths.py +0 -0
  48. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/release.py +0 -0
  49. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/store/__init__.py +0 -0
  50. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/store/acr.py +0 -0
  51. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/store/base.py +0 -0
  52. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/store/oss_discovery.py +0 -0
  53. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/store/registry.py +0 -0
  54. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/tasktoml.py +0 -0
  55. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo/validate.py +0 -0
  56. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo.egg-info/dependency_links.txt +0 -0
  57. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo.egg-info/entry_points.txt +0 -0
  58. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo.egg-info/requires.txt +0 -0
  59. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/instance_repo.egg-info/top_level.txt +0 -0
  60. {instance_repo-1.4.1.dev0 → instance_repo-1.6.0.dev2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: instance-repo
3
- Version: 1.4.1.dev0
3
+ Version: 1.6.0.dev2
4
4
  Summary: InstanceRepo SDK — 评测 Instance 统一存储客户端(SDK-only;控制面由 apiserver 承载)
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -20,7 +20,7 @@ from .errors import (
20
20
  RegistrationFailed, TransportError,
21
21
  )
22
22
 
23
- __version__ = "1.4.1.dev0"
23
+ __version__ = "1.6.0.dev2"
24
24
 
25
25
  __all__ = [
26
26
  "Repo", "load_instances",
@@ -37,3 +37,11 @@ ARTIFACT_PREFIX_ROOT = ""
37
37
  #: 注入。运行时也可经环境变量 AP_ARTIFACT_BUCKET_MAP(JSON)覆盖,优先级:
38
38
  #: 环境变量 > 本模块注入值 > 空(空则 artifact 下载报错并引导配置)。
39
39
  ARTIFACT_BUCKET_MAP_JSON = ""
40
+
41
+ #: 期望 replica policy(canonical 双桶对)的 JSON 串。**源码恒为空**;
42
+ #: 内网构建时由发布脚本注入(与 API_BASE 同模式——静态可审计、无
43
+ #: envvar 通道)。运行时解析为 ``ReplicasPolicy``(严格 JSON/拒重复
44
+ #: keys/构造验证);公共包通过显式构造参数传入有效 policy 亦可运行。
45
+ #: 优先级:显式构造参数 > 本模块注入值 > 缺失(缺失=新客户端零 IO
46
+ #: typed error)。
47
+ EXPECTED_REPLICA_POLICY_JSON = ""
@@ -0,0 +1,41 @@
1
+ """Private RFC3339 calendar parser with Python 3.10+ fraction behavior.
2
+
3
+ Wire values remain untouched at the call sites. Python datetime arithmetic uses
4
+ microseconds, matching 3.11+ fromisoformat truncation of finer fractions; expiry
5
+ is never rounded up. The original wire precision is not an integrity anchor.
6
+ """
7
+ from datetime import datetime, timedelta, timezone
8
+ import re
9
+
10
+ _UTC = re.compile(
11
+ r'([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})'
12
+ r'(?:\.([0-9]+))?(Z|[+-][0-9]{2}:[0-9]{2})\Z')
13
+
14
+
15
+ def parse_utc(value: str, *, allow_offset: bool = False) -> datetime:
16
+ """Validate calendar fields without version-specific ISO parsing.
17
+
18
+ Fraction processing is bounded to six digits, including arbitrarily precise
19
+ values permitted by existing read/write callers. Entries keeps its existing
20
+ one-to-nine-digit and byte bounds before invoking this helper.
21
+ """
22
+ match = _UTC.fullmatch(value) if type(value) is str and type(allow_offset) is bool else None
23
+ if match is None:
24
+ raise ValueError('invalid RFC3339 UTC datetime')
25
+ fraction = match.group(7) or ''
26
+ microsecond = int((fraction[:6] + '000000')[:6])
27
+ suffix = match.group(8)
28
+ hours, minutes = (0, 0) if suffix == 'Z' else (int(suffix[1:3]), int(suffix[4:6]))
29
+ if hours > 23 or minutes > 59 or (not allow_offset and (hours or minutes)):
30
+ raise ValueError('invalid RFC3339 datetime offset')
31
+ offset = (hours * 60 + minutes) * (-1 if suffix.startswith('-') else 1)
32
+ parsed = None
33
+ try:
34
+ parsed = datetime(*(int(match.group(n)) for n in range(1, 7)),
35
+ microsecond=microsecond, tzinfo=timezone(timedelta(minutes=offset)))
36
+ except ValueError:
37
+ pass
38
+ if parsed is None:
39
+ # Do not attach a calendar exception or raw wire string to the error.
40
+ raise ValueError('invalid RFC3339 UTC calendar datetime')
41
+ return parsed
@@ -0,0 +1,265 @@
1
+ """Exact key builder/parser for the currently registered RewardKit profile.
2
+
3
+ Only pyz/tf-rewardkit/rewardkit-VERSION-linux-x86_64.pyz is supported under
4
+ swe/user-data/ap-tool/. VERSION uses the approved verbatim PEP440 subset;
5
+ component/version128bytes, full key512bytes. No hash/fileID/version directory,
6
+ normalization, percent decoding, extra tool registration or platform expansion.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import re
11
+
12
+ from ..errors import SchemaError
13
+
14
+ #: ap-tool 域根(不可变;层级含义=ap-tool/包类型/工具名/文件名)。
15
+ TOOL_KEY_ROOT = "swe/user-data/ap-tool/"
16
+
17
+ #: 默认包类型(当前唯一正式类型;新类型须用户明确同意后扩展)。
18
+ DEFAULT_TOOL_KIND = "pyz"
19
+
20
+ #: tf-rewardkit 的文件名 stem(发布名=工具名去 ``tf-`` 前缀,H1 逐字节锚定)。
21
+
22
+ #: 工具名:小写字母开头,小写/数字/连字符(tf-rewardkit、t-rewardkit-2 等)。
23
+ # fullmatch:``tf-rewardkit.``/``tf rewardkit``/``tf\u2044rewardkit`` 全部拒收。
24
+ _TOOL_NAME_RE = re.compile(r"[a-z](?:[a-z0-9-]*[a-z0-9])?")
25
+
26
+ #: 包类型:纯小写字母(pyz;类型层不是自由命名空间)。
27
+ _KIND_RE = re.compile(r"[a-z]+")
28
+
29
+ #: 版本串:PEP440 **canonical** 形态(与真实 RewardKit publisher 对齐,
30
+ # Codex 退修 2026-10-30:已发版 1.1.0.dev2 必须支持;rc/post/local 支持)。
31
+ # canonical = N(.N)*[{a|b|rc}N][.postN][.devN][+local]——全部小写、无空白;
32
+ # local 段限定小写字母/数字/点(**禁连字符**:``-`` 是文件名中 version 与
33
+ # platform 的唯一分隔符,local 含 ``-`` 会造成 parse 歧义——``1.2.3+a-b``
34
+ # 无法区分 local 边界与 platform 开头,fail-closed 拒绝;PEP440 允许更宽的
35
+ # local 字符集,本协议收窄到文件名无歧义子集);大写/下划线等非 canonical
36
+ # 形态直接拒绝——不做归一化,调用方必须传 canonical(零静默改写,
37
+ # "No silent normalization")。
38
+ # 版本中的点属于版本语法本身——段字符白名单不适用于 version(单独校验)。
39
+ _VERSION_EPOCH = r"(?:0|[1-9][0-9]*)"
40
+ _VERSION_PRE = r"(?:(?:a|b|rc)(?:0|[1-9][0-9]*))?"
41
+ _VERSION_POST = r"(?:\.(?:post|rev|r)(?:0|[1-9][0-9]*))?"
42
+ _VERSION_DEV = r"(?:\.dev(?:0|[1-9][0-9]*))?"
43
+ _VERSION_LOCAL = r"(?:\+[a-z0-9]+(?:\.[a-z0-9]+)*)?"
44
+ _VERSION_RE = re.compile(
45
+ _VERSION_EPOCH + r"(?:\." + _VERSION_EPOCH + r")*"
46
+ + _VERSION_PRE + _VERSION_POST + _VERSION_DEV + _VERSION_LOCAL)
47
+
48
+ #: 平台串:小写/数字/连字符/下划线(linux-x86_64、linux-aarch64)。
49
+ _PLATFORM_RE = re.compile(r"[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?")
50
+
51
+ #: 文件名整体(stem-version-platform.pyz):自右锚定,version 段为 PEP440
52
+ #: canonical 形态(含 dev/rc/post/local;点与 ``+`` 属版本语法,不与
53
+ #: stem 的 ``-`` 分隔混淆——分隔符仍为 ``-``,platform 无点无 ``+``)。
54
+ _FILENAME_RE = re.compile(
55
+ r"(?P<stem>[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)"
56
+ r"-(?P<version>" + _VERSION_EPOCH + r"(?:\." + _VERSION_EPOCH + r")*"
57
+ + _VERSION_PRE + _VERSION_POST + _VERSION_DEV + _VERSION_LOCAL + r")"
58
+ r"-(?P<platform>[a-z0-9](?:[a-z0-9_-]*[a-z0-9_])?)"
59
+ r"\.pyz")
60
+
61
+ #: 组件段长上限(字节):tool_name/kind/platform 等段共用——与 API 侧
62
+ #: shared component 界限同值(128;Codex 03:30 统一裁定,替代先前的
63
+ #: 先行值 256)。防御异常超长输入(对齐 shared 路径安全风格)。
64
+ _MAX_SEGMENT_BYTES = 128
65
+
66
+ #: 最大版本串长度(字节,=A 侧 sharedNameVersionMaxLen=128;Codex 03:30
67
+ #: 统一裁定:64→128 放开——此前 64 是 B 侧先行值非约定,**勿任意收窄已
68
+ #: 约定有效版本**)。
69
+ MAX_VERSION_BYTES = 128
70
+
71
+ #: 最大文件名长度(字节):'pyz'+'-'+tool_name(≤128)+'-'+filename(≤388)
72
+ #: 的组成上界 ≈ 496,与整 key 界限 512 严格相容(不出现"filename 合法
73
+ #: 但 key 必然越界"的死区);生成侧与 parse 侧共用同一界限。
74
+ MAX_FILENAME_BYTES = 388
75
+
76
+ #: 整 key 长度(字节,=A/API 既有 key 上限 512;parse 前置检查——超长
77
+ #: key 在任何正则/切分前拒绝。Codex 03:30 统一裁定:1024→512)。
78
+ MAX_KEY_BYTES = 512
79
+
80
+
81
+ def _validate_segment(value: str, what: str, regex: re.Pattern) -> str:
82
+ """通用段校验:非空、长度有界、无首尾空白、白名单 fullmatch。
83
+
84
+ **不解码不变式**:``%`` 不在任何白名单字符集内——``%2e%2e``/``%2F``/
85
+ ``%252e`` 在此被字面拒绝;控制符/空白/反斜杠/非 ASCII(含 Unicode
86
+ 斜杠变体)同理(字符集仅 ASCII 小写字母/数字/连字符/点/下划线子集)。
87
+ """
88
+ if not isinstance(value, str):
89
+ raise SchemaError("ap-tool key component must be a string")
90
+ v = value
91
+ if not v:
92
+ raise SchemaError(f"ap-tool key segment {what!r} must not be empty")
93
+ if len(v.encode("utf-8")) > _MAX_SEGMENT_BYTES:
94
+ raise SchemaError(
95
+ f"ap-tool key segment {what!r} exceeds "
96
+ f"{_MAX_SEGMENT_BYTES} bytes: {v!r}")
97
+ if v != v.strip():
98
+ raise SchemaError(
99
+ f"ap-tool key segment {what!r} has leading/trailing "
100
+ f"whitespace: {v!r}")
101
+ if regex.fullmatch(v) is None:
102
+ raise SchemaError(
103
+ f"invalid ap-tool key segment {what!r}: {v!r} (allowed charset "
104
+ "is a restrictive ASCII subset; percent-encodings, uppercase, "
105
+ "whitespace, slashes and unicode variants are rejected "
106
+ "literally without decoding)")
107
+ return v
108
+
109
+
110
+ def validate_tool_version(version: str) -> str:
111
+ """校验版本串:PEP440 **canonical** 形态(与 RewardKit publisher 对齐)。
112
+
113
+ 合法:``1.2.3``、``1.1.0.dev2``(已发版)、``1.2.3rc1``、``1.2.3.post1``、
114
+ ``1.0a1``、``1.2.3+local.7``(local 限定小写/数字/点/连字符子集)。
115
+ 拒绝:非 canonical(``1.2.3.RC1`` 大写、``1.2.3-rc1`` 连字符分隔、
116
+ ``1.02.3`` 前导零、``v1.2`` 前缀、``1.2.3..post1`` 空段)、斜杠/点段
117
+ 路径形态、编码逃逸(``%`` 不在字符集)、控制符、空白。
118
+ **零静默归一化**:版本原样进入 key(canonical 要求由调用方保证——
119
+ 非 canonical 输入被拒而非被改写;validate 返回值恒等于输入原串)。
120
+ """
121
+ if not isinstance(version, str):
122
+ raise SchemaError("ap-tool version must be a string")
123
+ v = version
124
+ if not v:
125
+ raise SchemaError("ap-tool version must not be empty")
126
+ # 长度界限先于任何正则(Codex 边界 review:超长数字版本曾通过)。
127
+ if len(v.encode("utf-8")) > MAX_VERSION_BYTES:
128
+ raise SchemaError(
129
+ f"ap-tool version exceeds {MAX_VERSION_BYTES} bytes: "
130
+ f"{len(v.encode('utf-8'))} bytes")
131
+ if v != v.strip():
132
+ raise SchemaError(
133
+ f"invalid ap-tool version {v!r}: leading/trailing whitespace")
134
+ if _VERSION_RE.fullmatch(v) is None:
135
+ raise SchemaError(
136
+ f"invalid ap-tool version {v!r}: must be PEP440 canonical "
137
+ "(e.g. 1.2.3, 1.1.0.dev2, 1.2.3rc1, 1.2.3.post1, "
138
+ "1.2.3+local; no leading zeros, uppercase, hyphen separators, "
139
+ "prefix letters or escapes)")
140
+ return v
141
+
142
+
143
+ def validate_tool_platform(platform: str) -> str:
144
+ """校验平台串:小写字母/数字/连字符/下划线(linux-x86_64 等)。"""
145
+ return _validate_segment(platform, "platform", _PLATFORM_RE)
146
+
147
+
148
+ def validate_tool_name(tool_name: str) -> str:
149
+ """校验工具名:小写字母开头的小写/数字/连字符段(tf-rewardkit 等)。"""
150
+ return _validate_segment(tool_name, "tool_name", _TOOL_NAME_RE)
151
+
152
+
153
+ def validate_tool_kind(kind: str) -> str:
154
+ """校验包类型段:纯小写字母(pyz)。新类型须用户明确同意后扩展。"""
155
+ return _validate_segment(kind, "kind", _KIND_RE)
156
+
157
+
158
+ def tool_filename(tool_name: str, version: str, *,
159
+ platform: str = "linux-x86_64") -> str:
160
+ """version→文件名映射(协议锚定;version **verbatim** 原样入名)。
161
+
162
+ tf-rewardkit + 1.1.0.dev2 → ``rewardkit-1.1.0.dev2-linux-x86_64.pyz``;
163
+ tf-rewardkit + 1.2.3 → ``rewardkit-1.2.3-linux-x86_64.pyz``:
164
+ Registered tool identity maps tf-rewardkit directory to rewardkit artifact.版本串不做任何
165
+ 归一化改写(canonical 由 validate 保证,文件名所见即调用方所传)。
166
+ """
167
+ name = validate_tool_name(tool_name)
168
+ ver = validate_tool_version(version)
169
+ plat = validate_tool_platform(platform)
170
+ if name != "tf-rewardkit" or plat != "linux-x86_64":
171
+ raise SchemaError("unsupported tool profile; only tf-rewardkit/linux-x86_64 is registered")
172
+ stem = "rewardkit"
173
+ if not stem:
174
+ raise SchemaError(
175
+ f"invalid ap-tool tool_name {tool_name!r}: 'tf-' alone has no "
176
+ "file stem")
177
+ filename = f"{stem}-{ver}-{plat}.pyz"
178
+ # 生成侧界限(Codex 边界 review 2026-10-31):合成文件名超过上限拒绝。
179
+ if len(filename.encode("utf-8")) > MAX_FILENAME_BYTES:
180
+ raise SchemaError(
181
+ f"ap-tool filename exceeds {MAX_FILENAME_BYTES} bytes: "
182
+ f"{len(filename.encode('utf-8'))} bytes")
183
+ return filename
184
+
185
+
186
+ def tool_object_key(tool_name: str, version: str, *,
187
+ kind: str = DEFAULT_TOOL_KIND,
188
+ platform: str = "linux-x86_64") -> str:
189
+ """正式工具对象 key(唯一入口;两桶同 key 的单一来源)。
190
+
191
+ 例:``tool_object_key("tf-rewardkit", "1.2.3")`` 逐字节返回
192
+ ``swe/user-data/ap-tool/pyz/tf-rewardkit/rewardkit-1.2.3-linux-x86_64.pyz``;
193
+ ``tool_object_key("tf-rewardkit", "1.1.0.dev2")`` 逐字节返回
194
+ ``swe/user-data/ap-tool/pyz/tf-rewardkit/rewardkit-1.1.0.dev2-linux-x86_64.pyz``
195
+ (实际已发版 key,roundtrip 锚点;A 报告 §8.4 H1-H5 同构)。
196
+
197
+ 设计约束(差异B §6.2):SHA/size/generation 不入路径;调用方对主/从桶
198
+ 复用**同一返回值**(结构上杜绝两桶 key 分叉)。
199
+ """
200
+ name = validate_tool_name(tool_name)
201
+ k = validate_tool_kind(kind)
202
+ if k != "pyz":
203
+ raise SchemaError("unsupported tool package; only pyz is registered")
204
+ filename = tool_filename(tool_name, version, platform=platform)
205
+ key = f"{TOOL_KEY_ROOT}{k}/{name}/{filename}"
206
+ # 整 key 界限(生成侧;parse 侧同界限前置——两侧同一常量)。
207
+ if len(key.encode("utf-8")) > MAX_KEY_BYTES:
208
+ raise SchemaError(
209
+ f"ap-tool key exceeds {MAX_KEY_BYTES} bytes: "
210
+ f"{len(key.encode('utf-8'))} bytes")
211
+ return key
212
+
213
+
214
+ def parse_tool_object_key(key: str) -> dict:
215
+ """解析正式 key 为协议字段(消费侧对账/下载 URL 校验用,H2 同口径)。
216
+
217
+ 返回 ``{tool_name, kind, version, platform, filename}``;任何偏离协议
218
+ 模板的 key(域根下段数≠3、非 pyz 扩展、文件名不匹配
219
+ ``stem-version-platform.pyz``、stem 与 tool_name 不一致)拒绝——用于把
220
+ download URL / 回执 final_key 与正式协议三方对账(A 报告 H2/H3)。
221
+ """
222
+ v = str(key or "")
223
+ # parse 前置整 key 界限(Codex 边界 review 2026-10-31:先于任何正则/
224
+ # 切分——超长 key 在最前面拒绝,不进入解析路径)。
225
+ if len(v.encode("utf-8")) > MAX_KEY_BYTES:
226
+ raise SchemaError(
227
+ f"ap-tool key exceeds {MAX_KEY_BYTES} bytes: "
228
+ f"{len(v.encode('utf-8'))} bytes")
229
+ if not v.startswith(TOOL_KEY_ROOT):
230
+ raise SchemaError(
231
+ f"key {v!r} is not under ap-tool root {TOOL_KEY_ROOT!r}")
232
+ rest = v[len(TOOL_KEY_ROOT):]
233
+ segs = rest.split("/")
234
+ if len(segs) != 3:
235
+ raise SchemaError(
236
+ f"ap-tool key must have exactly 3 segments under the root "
237
+ f"(kind/tool_name/filename), got {len(segs)}: {v!r}")
238
+ kind, name, filename = segs
239
+ validate_tool_kind(kind)
240
+ validate_tool_name(name)
241
+ # parse 侧文件名界限(先于 _FILENAME_RE——与生成侧同常量)。
242
+ if len(filename.encode("utf-8")) > MAX_FILENAME_BYTES:
243
+ raise SchemaError(
244
+ f"ap-tool key filename exceeds {MAX_FILENAME_BYTES} bytes: "
245
+ f"{len(filename.encode('utf-8'))} bytes")
246
+ if not filename.endswith(".pyz"):
247
+ raise SchemaError(
248
+ f"ap-tool key filename must end with .pyz: {filename!r}")
249
+ m = _FILENAME_RE.fullmatch(filename)
250
+ if m is None:
251
+ raise SchemaError(
252
+ f"ap-tool key filename malformed (expected "
253
+ f"stem-version-platform.pyz): {filename!r}")
254
+ stem = m.group("stem")
255
+ expected_stem = name[len("tf-"):] if name.startswith("tf-") else name
256
+ if stem != expected_stem:
257
+ raise SchemaError(
258
+ f"ap-tool key filename stem {stem!r} does not match tool_name "
259
+ f"{name!r} (expected {expected_stem!r})")
260
+ validate_tool_version(m.group("version"))
261
+ validate_tool_platform(m.group("platform"))
262
+ if tool_object_key(name, m.group("version"), kind=kind, platform=m.group("platform")) != v:
263
+ raise SchemaError("ap-tool key does not match registered tool profile")
264
+ return {"tool_name": name, "kind": kind, "version": m.group("version"),
265
+ "platform": m.group("platform"), "filename": filename}
@@ -26,6 +26,7 @@ OSS bucket/endpoint/region 或 ACR registry/namespace,这些一律运行时取
26
26
  """
27
27
  from __future__ import annotations
28
28
 
29
+ import contextlib
29
30
  import logging
30
31
  import os
31
32
  import warnings
@@ -230,6 +231,12 @@ class Profile:
230
231
  #: 内网时置 True,配合 ``acr_public_host``(repo-config 的 public_registry)
231
232
  #: 生效;公网值缺失时静默回落到 ``acr_host``。
232
233
  prefer_public_registry: bool = False
234
+ #: user-data 域独立 OSS bucket(F2-R1):显式配置优先于 scope=user-data
235
+ #: 自动发现(零发现网络调用)。空 = 走发现。
236
+ user_data_bucket: str = ""
237
+ #: user-data 域独立 OSS endpoint(F2-R1):显式配置优先(须与 user_data_bucket
238
+ #: 配套使用——bucket 走发现时该字段忽略,endpoint 以发现结果为准)。空=不覆盖。
239
+ user_data_endpoint: str = ""
233
240
 
234
241
  @property
235
242
  def effective_acr_host(self) -> str:
@@ -311,7 +318,8 @@ def load_profile(name: str = "default", *, overrides: dict | None = None,
311
318
  # 只接受已知字段,避免把 api_env 之类误当数据面字段;
312
319
  # prefer_public_registry 单独取(非寻址字段,不进 **cfg 传 Profile)。
313
320
  for k in ("oss_bucket", "acr_host", "acr_namespace", "acr_public_host",
314
- "oss_prefix", "scaffold_bucket", "scaffold_root", "api_base"):
321
+ "oss_prefix", "scaffold_bucket", "scaffold_root", "api_base",
322
+ "user_data_bucket", "user_data_endpoint"):
315
323
  if k in overrides and overrides[k] is not None:
316
324
  cfg[k] = overrides[k]
317
325
  sources[k] = "explicit"
@@ -324,6 +332,8 @@ def load_profile(name: str = "default", *, overrides: dict | None = None,
324
332
  prefer_public = os.environ.get(ENV_ACR_PREFER_PUBLIC, "").strip().lower() \
325
333
  in ("1", "true", "on")
326
334
  return Profile(name=name, api_env=env,
335
+ user_data_bucket=str(cfg.pop("user_data_bucket", "") or "").strip(),
336
+ user_data_endpoint=str(cfg.pop("user_data_endpoint", "") or "").strip(),
327
337
  cluster=str(cluster_value).strip(),
328
338
  storage_env=normalize_storage_env(storage_value),
329
339
  metadata_model=requested_model,
@@ -486,3 +496,197 @@ def clear_cluster_map_cache() -> None:
486
496
  global _cluster_map_cache, _cluster_map_expiry
487
497
  _cluster_map_cache = {}
488
498
  _cluster_map_expiry = {}
499
+
500
+
501
+ # ── scope=user-data 独立配置发现(T-SDK-USERDATA,对齐 apiserver F1 schema1)────
502
+
503
+ #: user-data scope 的合法取值(apiserver repo_config_handler.repoConfigScopeUserData)。
504
+ USER_DATA_SCOPE = "user-data"
505
+
506
+ #: user-data 响应契约版本(apiserver userDataSchemaVersion;SDK 只认 1)。
507
+ USER_DATA_SCHEMA_VERSION = 1
508
+
509
+ #: user-data 域根前缀锚(apiserver userDataPrefixRoot / Repo.USER_DATA_ROOT)。
510
+ USER_DATA_PREFIX_ROOT = "swe/user-data/users/"
511
+
512
+
513
+ @dataclass(frozen=True)
514
+ class UserDataConfig:
515
+ """scope=user-data 的解析结果:仅 OSS 寻址 + 能力声明,**不含任何密钥**。
516
+
517
+ ``production_read_enabled`` / ``production_write_enabled`` 只是当前部署的
518
+ gate **能力声明**,不构成授权——真实 STS 每次按认证主体与 RBAC 重新判定,
519
+ 调用方不得据此假定签发结果。旧服务(不认识 scope)经 ``allow_fallback``
520
+ 兼容时,这两个字段恒为 False(能力未知 → 关闭,不猜生产开启)。
521
+ """
522
+
523
+ schema_version: int = USER_DATA_SCHEMA_VERSION
524
+ storage_env: str = ""
525
+ prefix_root: str = USER_DATA_PREFIX_ROOT
526
+ production_read_enabled: bool = False
527
+ production_write_enabled: bool = False
528
+ oss_bucket: str = ""
529
+ oss_prefix: str = ""
530
+ oss_endpoint: str = ""
531
+ oss_region: str = ""
532
+
533
+ def to_profile_overrides(self) -> dict:
534
+ """转成 user-data **独立** profile overrides(只含 OSS 寻址 + storage_env)。
535
+
536
+ 独立性:不含任何 acr_*(user-data 域只用 OSS、不要求 ACR)、不含 cluster。
537
+ 用于为 user-data 上下文单独构造 Store/Content;**不要**并入 Dataset/
538
+ artifact 的 profile——会污染其寻址。"""
539
+ ov = {"storage_env": self.storage_env,
540
+ "oss_bucket": self.oss_bucket,
541
+ "oss_prefix": self.oss_prefix}
542
+ if self.oss_endpoint:
543
+ ov["oss_endpoint"] = self.oss_endpoint
544
+ if self.oss_region:
545
+ ov["oss_region"] = self.oss_region
546
+ return ov
547
+
548
+
549
+ def _parse_user_data_config(data: dict, *, storage_env: str) -> "UserDataConfig":
550
+ """校验并解析 scope=user-data 响应体(schema_version=1 契约,fail-closed)。
551
+
552
+ P2-③ 严格校验:schema_version 必须是**精确整数 1**(bool 是 int 子类须显式排除,
553
+ True==1 陷阱);能力字段必须是真 bool("false" 字符串不得被 truthy);oss 必须
554
+ dict 且 bucket 非空;storage_env 缺失拒绝(不默默取调用方 env 变 dev)。
555
+ """
556
+ from ..errors import SchemaError
557
+ if not isinstance(data, dict):
558
+ raise SchemaError("unexpected user-data repo-config response shape")
559
+ v = data.get("schema_version")
560
+ if isinstance(v, bool) or not isinstance(v, int) or v != USER_DATA_SCHEMA_VERSION:
561
+ raise SchemaError(
562
+ f"unsupported user-data repo-config schema_version: {v!r} "
563
+ f"(supported: exact int {USER_DATA_SCHEMA_VERSION})")
564
+ prefix_root = str(data.get("prefix_root") or "")
565
+ if prefix_root != USER_DATA_PREFIX_ROOT:
566
+ raise SchemaError(
567
+ f"user-data prefix_root mismatch: {prefix_root!r} "
568
+ f"(expected {USER_DATA_PREFIX_ROOT!r}; server/SDK 常量须一致)")
569
+ for key in ("production_read_enabled", "production_write_enabled"):
570
+ flag = data.get(key)
571
+ if not isinstance(flag, bool):
572
+ raise SchemaError(
573
+ f"user-data {key} must be a boolean, got {flag!r}")
574
+ oss = data.get("oss")
575
+ if not isinstance(oss, dict) or not str(oss.get("bucket") or "").strip():
576
+ raise SchemaError(
577
+ "user-data repo-config oss must be an object with non-empty bucket "
578
+ f"(fail-closed), got {oss!r}")
579
+ env_raw = data.get("storage_env")
580
+ if not isinstance(env_raw, str) or not env_raw.strip():
581
+ raise SchemaError(
582
+ "user-data repo-config storage_env is required (missing/empty; "
583
+ "refusing to silently default)")
584
+ env = normalize_storage_env(env_raw)
585
+ return UserDataConfig(
586
+ schema_version=USER_DATA_SCHEMA_VERSION,
587
+ storage_env=env,
588
+ prefix_root=prefix_root,
589
+ production_read_enabled=data["production_read_enabled"],
590
+ production_write_enabled=data["production_write_enabled"],
591
+ oss_bucket=str(oss.get("bucket") or ""),
592
+ oss_prefix=str(oss.get("prefix") or ""),
593
+ oss_endpoint=str(oss.get("endpoint") or ""),
594
+ oss_region=str(oss.get("region") or ""),
595
+ )
596
+
597
+
598
+ def fetch_user_data_config(transport, *, cluster: str = "", storage_env: str = "",
599
+ allow_fallback: bool = False) -> "UserDataConfig":
600
+ """从 apiserver ``GET /apis/v1/repo-config?…&scope=user-data`` 拉取 user-data
601
+ 独立配置(schema1:仅 OSS 寻址 + 生产读写能力声明,无密钥)。
602
+
603
+ * ``cluster`` / ``storage_env`` 二选一;**同时给出时两个键都下发服务端**
604
+ (P1-①:不得丢显式 env——服务端按 cluster 解析并做同语义冲突 400)。
605
+ * **客户端预检**:cluster→storage_env 映射表(真实形状 ``{"clusters": {name:
606
+ env}}`` 封套)与显式 env 冲突 → SchemaError(对齐 apiserver 400
607
+ cluster_env_conflict:不静默换环境)。映射请求 401/403/5xx 等故障**原样
608
+ 抛出**(不得吞成"查不到"继续——那会在权限故障时静默跳过冲突检查);
609
+ 仅"200 但封套里无该 cluster"(新集群未配)才跳过预检交服务端兜底。
610
+ * **响应 env 核对**(P1-①):scope 响应的 storage_env 与显式 env 不一致 →
611
+ SchemaError(防服务端映射变更后的静默换环境)。
612
+ * **旧服务兼容**(``allow_fallback=True``):400 invalid scope 时回退一次
613
+ **无 scope 旧 repo-config(保留 cluster 与显式 env 双键)**;旧响应的
614
+ storage_env 与显式 env 也不一致 → SchemaError。能力字段恒 False。
615
+ * 认证(401)/权限(403)/5xx/404 等不当版本 fallback,原样抛出。
616
+ """
617
+ from ..errors import SchemaError
618
+ explicit_env = str(storage_env or "").strip()
619
+ resolved_env = ""
620
+ if cluster:
621
+ cluster = resolve_cluster(transport, cluster)
622
+ # 客户端预检:cluster→env 映射(真实封套 {"clusters": {name: env}})。
623
+ # 请求失败(401/403/DB 故障等异常)原样抛——不得吞成兼容继续。
624
+ m = transport.get("/apis/v1/repo-config/cluster-storage-env-map")
625
+ if not isinstance(m, dict):
626
+ raise SchemaError("unexpected cluster-storage-env-map response shape")
627
+ entries = m.get("clusters") if isinstance(m.get("clusters"), dict) else m
628
+ if isinstance(entries, dict):
629
+ resolved_env = str(entries.get(cluster) or "").strip()
630
+ if resolved_env and explicit_env and explicit_env != resolved_env:
631
+ raise SchemaError(
632
+ f"cluster {cluster!r} maps to storage_env {resolved_env!r}; "
633
+ f"conflicts with storage_env {explicit_env!r}")
634
+ query = {"cluster": cluster, "scope": USER_DATA_SCOPE}
635
+ if explicit_env:
636
+ query["storage_env"] = explicit_env # P1-①:双键一起下发
637
+ elif explicit_env:
638
+ query = {"storage_env": explicit_env, "scope": USER_DATA_SCOPE}
639
+ else:
640
+ raise SchemaError("fetch_user_data_config requires 'cluster' or 'storage_env'")
641
+ try:
642
+ data = transport.get("/apis/v1/repo-config?" + urlencode(query))
643
+ except Exception as exc:
644
+ status = getattr(exc, "status", None)
645
+ msg = str(getattr(exc, "message", "") or exc)
646
+ is_invalid_scope = (status == 400 and "invalid scope" in msg.lower())
647
+ if allow_fallback and is_invalid_scope:
648
+ # 旧服务不认识 scope:回退旧 repo-config(保留 cluster + 显式 env 双键)。
649
+ if cluster:
650
+ old_query = {"cluster": cluster}
651
+ if explicit_env:
652
+ old_query["storage_env"] = explicit_env
653
+ elif explicit_env:
654
+ old_query = {"storage_env": explicit_env}
655
+ else:
656
+ raise SchemaError(
657
+ "fallback requires cluster or storage_env") from exc
658
+ old = transport.get("/apis/v1/repo-config?" + urlencode(old_query))
659
+ if not isinstance(old, dict):
660
+ raise SchemaError(
661
+ "unexpected legacy repo-config response shape") from exc
662
+ oss = old.get("oss")
663
+ if not isinstance(oss, dict) or not str(oss.get("bucket") or "").strip():
664
+ raise SchemaError(
665
+ "legacy repo-config oss must be an object with "
666
+ "non-empty bucket") from exc
667
+ env_raw = str(old.get("storage_env") or "").strip()
668
+ # P1-①:旧响应 env 与显式 env 不一致 → 拒绝(不静默换环境)。
669
+ if env_raw and explicit_env and env_raw != explicit_env:
670
+ raise SchemaError(
671
+ f"legacy repo-config resolved storage_env {env_raw!r} "
672
+ f"conflicts with explicit storage_env {explicit_env!r}")
673
+ env = normalize_storage_env(env_raw or explicit_env)
674
+ return UserDataConfig(
675
+ schema_version=USER_DATA_SCHEMA_VERSION,
676
+ storage_env=env,
677
+ production_read_enabled=False, # 能力未知 → 关闭,不猜
678
+ production_write_enabled=False,
679
+ oss_bucket=str(oss.get("bucket") or ""),
680
+ oss_prefix=str(oss.get("prefix") or ""),
681
+ oss_endpoint=str(oss.get("endpoint") or ""),
682
+ oss_region=str(oss.get("region") or ""),
683
+ )
684
+ raise
685
+ cfg = _parse_user_data_config(data, storage_env=explicit_env)
686
+ # P1-①:响应 env 与显式 env 不一致 → 拒绝(不静默换环境)。
687
+ if explicit_env and cfg.storage_env and \
688
+ cfg.storage_env != normalize_storage_env(explicit_env):
689
+ raise SchemaError(
690
+ f"repo-config resolved storage_env {cfg.storage_env!r} conflicts "
691
+ f"with explicit storage_env {explicit_env!r}")
692
+ return cfg