pythinker-code 0.57.0__py3-none-any.whl → 0.59.0__py3-none-any.whl
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.
- pythinker_code/CHANGELOG.md +59 -0
- pythinker_code/agents/default/system.md +1 -1
- pythinker_code/agentspec.py +171 -11
- pythinker_code/auth/__init__.py +4 -2
- pythinker_code/auth/platforms.py +37 -15
- pythinker_code/auth/z_ai.py +251 -187
- pythinker_code/background/agent_runner.py +4 -0
- pythinker_code/background/manager.py +8 -0
- pythinker_code/benchmark/toolset_characterization.py +1118 -0
- pythinker_code/cli/__init__.py +83 -4
- pythinker_code/cli/system_prompt.py +2 -1
- pythinker_code/llm.py +57 -227
- pythinker_code/provider_compatibility.py +401 -0
- pythinker_code/skill/__init__.py +139 -8
- pythinker_code/skill/catalog.py +509 -0
- pythinker_code/soul/agent.py +168 -69
- pythinker_code/soul/btw.py +35 -10
- pythinker_code/soul/compaction_restore.py +3 -3
- pythinker_code/soul/context.py +871 -220
- pythinker_code/soul/dynamic_injection.py +120 -80
- pythinker_code/soul/dynamic_injections/agent_list.py +2 -1
- pythinker_code/soul/dynamic_injections/model_defense.py +47 -12
- pythinker_code/soul/dynamic_injections/permissions_state.py +60 -9
- pythinker_code/soul/pythinkersoul.py +510 -219
- pythinker_code/soul/request_assembly.py +770 -0
- pythinker_code/soul/request_lifecycle.py +481 -0
- pythinker_code/soul/request_primitives.py +34 -0
- pythinker_code/soul/slash.py +64 -2
- pythinker_code/soul/tool_execution.py +945 -0
- pythinker_code/soul/toolset.py +73 -638
- pythinker_code/subagents/catalogue.py +398 -0
- pythinker_code/subagents/core.py +36 -8
- pythinker_code/subagents/discovery.py +139 -22
- pythinker_code/subagents/git_context.py +179 -82
- pythinker_code/subagents/review_target.py +715 -0
- pythinker_code/subagents/runner.py +19 -2
- pythinker_code/telemetry/metrics.py +47 -0
- pythinker_code/tools/agent/__init__.py +102 -17
- pythinker_code/tools/agent/description.md +4 -0
- pythinker_code/tools/skill/__init__.py +29 -10
- pythinker_code/ui/shell/oauth.py +32 -16
- pythinker_code/ui/shell/slash.py +17 -4
- pythinker_code/ui/shell/usage_adapters/__init__.py +5 -0
- pythinker_code/ui/shell/usage_adapters/z_ai.py +33 -0
- pythinker_code/ui/shell/visualize/_activity_tree.py +2 -5
- pythinker_code/ui/shell/visualize/_blocks.py +55 -2
- pythinker_code/utils/frontmatter.py +14 -3
- pythinker_code/utils/trust.py +16 -0
- pythinker_code/web/static/assets/architecture-7EHR7CIX-3gv838NK.js +1 -0
- pythinker_code/web/static/assets/{architectureDiagram-3BPJPVTR-CMjTHbQr.js → architectureDiagram-3BPJPVTR-DLLFeZrO.js} +1 -1
- pythinker_code/web/static/assets/{blockDiagram-GPEHLZMM-DbNV66qZ.js → blockDiagram-GPEHLZMM-CkkDeCg3.js} +1 -1
- pythinker_code/web/static/assets/{bootstrap-DHJiVjsg.js → bootstrap-CYaJoziy.js} +5 -5
- pythinker_code/web/static/assets/{c4Diagram-AAUBKEIU-_L7KfhiY.js → c4Diagram-AAUBKEIU-Bp0uYF88.js} +1 -1
- pythinker_code/web/static/assets/channel-CkHUjHH1.js +1 -0
- pythinker_code/web/static/assets/{chunk-2J33WTMH-Dsc-vkPL.js → chunk-2J33WTMH-fp3JfPmd.js} +1 -1
- pythinker_code/web/static/assets/{chunk-3OPIFGDE-DkPxKRGJ.js → chunk-3OPIFGDE-wcHktjqJ.js} +1 -1
- pythinker_code/web/static/assets/{chunk-5ZQYHXKU-DKsi-Xj_.js → chunk-5ZQYHXKU-0AKuUvLh.js} +1 -1
- pythinker_code/web/static/assets/{chunk-727SXJPM-DusG3GJh.js → chunk-727SXJPM-DyxXlkLC.js} +1 -1
- pythinker_code/web/static/assets/{chunk-AQP2D5EJ-F4_a1a4u.js → chunk-AQP2D5EJ-BXKoDDbF.js} +1 -1
- pythinker_code/web/static/assets/{chunk-CSCIHK7Q-9L6bDdXh.js → chunk-CSCIHK7Q-D2HuONst.js} +1 -1
- pythinker_code/web/static/assets/{chunk-JAPRZBRM-BavSbd1t.js → chunk-JAPRZBRM-mrWIOJ3Z.js} +4 -4
- pythinker_code/web/static/assets/{chunk-KSCS5N6A-D9NmS9a_.js → chunk-KSCS5N6A-HBBRdKIv.js} +1 -1
- pythinker_code/web/static/assets/{chunk-L5ZTLDWV-BSMfQ2IM.js → chunk-L5ZTLDWV-DXKFCZsy.js} +1 -1
- pythinker_code/web/static/assets/{chunk-LZXEDZCA-r4sJTTgY.js → chunk-LZXEDZCA-B3ZD-FXA.js} +2 -2
- pythinker_code/web/static/assets/{chunk-ND2GUHAM-BL7ERbud.js → chunk-ND2GUHAM-DuP52a96.js} +1 -1
- pythinker_code/web/static/assets/{chunk-NZK2D7GU-G9jWyqp3.js → chunk-NZK2D7GU-CnbADCJ7.js} +1 -1
- pythinker_code/web/static/assets/{chunk-O5CBEL6O-QBSTZ593.js → chunk-O5CBEL6O-pVVcoftv.js} +1 -1
- pythinker_code/web/static/assets/{chunk-WU5MYG2G-DYANwips.js → chunk-WU5MYG2G-BAGZBXu1.js} +1 -1
- pythinker_code/web/static/assets/classDiagram-4FO5ZUOK-dmKXWwan.js +1 -0
- pythinker_code/web/static/assets/classDiagram-v2-Q7XG4LA2-dmKXWwan.js +1 -0
- pythinker_code/web/static/assets/{code-block-IT6T5CEO-uNhBh7Jk.js → code-block-IT6T5CEO-DYAyge1a.js} +1 -1
- pythinker_code/web/static/assets/{dagre-BM42HDAG-Cb91t8pu.js → dagre-BM42HDAG-BKxxBFuu.js} +1 -1
- pythinker_code/web/static/assets/{diagram-2AECGRRQ-CLZIVQzB.js → diagram-2AECGRRQ-G94XlJaV.js} +1 -1
- pythinker_code/web/static/assets/{diagram-5GNKFQAL-B6DPBt7v.js → diagram-5GNKFQAL-CiSAYUQ6.js} +1 -1
- pythinker_code/web/static/assets/{diagram-KO2AKTUF-vb7QYejh.js → diagram-KO2AKTUF-BbSqB6Ns.js} +1 -1
- pythinker_code/web/static/assets/{diagram-LMA3HP47-DdFQHXp3.js → diagram-LMA3HP47-G8Eh-i79.js} +1 -1
- pythinker_code/web/static/assets/{diagram-OG6HWLK6-nJ1_wWcq.js → diagram-OG6HWLK6-D7IzVrGq.js} +1 -1
- pythinker_code/web/static/assets/{dist-D1EQtQ8U.js → dist-CZrlFtNU.js} +1 -1
- pythinker_code/web/static/assets/{erDiagram-TEJ5UH35-sZ8vCcB8.js → erDiagram-TEJ5UH35-BS2kAHFz.js} +1 -1
- pythinker_code/web/static/assets/eventmodeling-FCH6USID-D3DrnYSf.js +1 -0
- pythinker_code/web/static/assets/{flowDiagram-I6XJVG4X-NagVrMCI.js → flowDiagram-I6XJVG4X-C_F9vMtr.js} +1 -1
- pythinker_code/web/static/assets/{ganttDiagram-6RSMTGT7-D-1_XhUQ.js → ganttDiagram-6RSMTGT7-B2jC1UUf.js} +1 -1
- pythinker_code/web/static/assets/{gitGraph-WXDBUCRP-DT3QZQPP.js → gitGraph-WXDBUCRP-B0vzLEp0.js} +1 -1
- pythinker_code/web/static/assets/{gitGraphDiagram-PVQCEYII-D8r0djS7.js → gitGraphDiagram-PVQCEYII-B-wPebOF.js} +1 -1
- pythinker_code/web/static/assets/{index-2eH9dBB3.js → index-laCurKYw.js} +2 -2
- pythinker_code/web/static/assets/{info-J43DQDTF-CsKNLP2n.js → info-J43DQDTF-DUjhI1nz.js} +1 -1
- pythinker_code/web/static/assets/{infoDiagram-5YYISTIA-CIiPQAr1.js → infoDiagram-5YYISTIA-Cbwstbdp.js} +1 -1
- pythinker_code/web/static/assets/{ishikawaDiagram-YF4QCWOH-B0xZv1Ps.js → ishikawaDiagram-YF4QCWOH-BkycF4AV.js} +1 -1
- pythinker_code/web/static/assets/{journeyDiagram-JHISSGLW-CSiKJ4Vm.js → journeyDiagram-JHISSGLW-CJ42Wqhp.js} +1 -1
- pythinker_code/web/static/assets/{kanban-definition-UN3LZRKU-BYVbc39d.js → kanban-definition-UN3LZRKU-CwqoUk88.js} +1 -1
- pythinker_code/web/static/assets/{line-DHysIuAr.js → line-D3Csi8Mz.js} +1 -1
- pythinker_code/web/static/assets/mermaid-VLURNSYL-CxqVR2Qj.js +1 -0
- pythinker_code/web/static/assets/{mermaid-parser.core-1Xw2pmTV.js → mermaid-parser.core-Be6EdzXv.js} +2 -2
- pythinker_code/web/static/assets/{mermaid.core-yguwm67R.js → mermaid.core-Bry8j9-Y.js} +3 -3
- pythinker_code/web/static/assets/{mindmap-definition-RKZ34NQL-BzbokPP4.js → mindmap-definition-RKZ34NQL-r7fWzBYs.js} +1 -1
- pythinker_code/web/static/assets/{packet-YPE3B663-DvwZvo4I.js → packet-YPE3B663-C9dNvYXo.js} +1 -1
- pythinker_code/web/static/assets/{pie-LRSECV5Y-D3m6fHWL.js → pie-LRSECV5Y-BAAPLVkr.js} +1 -1
- pythinker_code/web/static/assets/{pieDiagram-4H26LBE5-Ds6AGwnk.js → pieDiagram-4H26LBE5-vWjWwnGH.js} +1 -1
- pythinker_code/web/static/assets/{quadrantDiagram-W4KKPZXB-DX8m3F04.js → quadrantDiagram-W4KKPZXB-B_yvrBi5.js} +1 -1
- pythinker_code/web/static/assets/{radar-GUYGQ44K-Bma1Der9.js → radar-GUYGQ44K-dthF6sP5.js} +1 -1
- pythinker_code/web/static/assets/{requirementDiagram-4Y6WPE33-OcYBKZGh.js → requirementDiagram-4Y6WPE33-BdIjnY_y.js} +1 -1
- pythinker_code/web/static/assets/{sankeyDiagram-5OEKKPKP-qZTKBl0U.js → sankeyDiagram-5OEKKPKP-BO-wztnd.js} +1 -1
- pythinker_code/web/static/assets/{sequenceDiagram-3UESZ5HK-Dat3afNR.js → sequenceDiagram-3UESZ5HK-D4OTlb5_.js} +1 -1
- pythinker_code/web/static/assets/{stateDiagram-AJRCARHV-C25VdSrM.js → stateDiagram-AJRCARHV-DbM9zIup.js} +1 -1
- pythinker_code/web/static/assets/stateDiagram-v2-BHNVJYJU-Bx9jf2eN.js +1 -0
- pythinker_code/web/static/assets/{timeline-definition-PNZ67QCA-BO52-l4o.js → timeline-definition-PNZ67QCA-Bdnq296j.js} +1 -1
- pythinker_code/web/static/assets/{treeView-BLDUP644-wTS6XClR.js → treeView-BLDUP644-CfSkXca9.js} +1 -1
- pythinker_code/web/static/assets/{treemap-LRROVOQU-H-Ce4Zv7.js → treemap-LRROVOQU-BfIIMFqt.js} +1 -1
- pythinker_code/web/static/assets/{vennDiagram-CIIHVFJN-C8VDHwNL.js → vennDiagram-CIIHVFJN-Bh4a039Q.js} +1 -1
- pythinker_code/web/static/assets/{wardley-L42UT6IY-CWdFDSTM.js → wardley-L42UT6IY-z1eD6dJ1.js} +1 -1
- pythinker_code/web/static/assets/{wardleyDiagram-YWT4CUSO-Cl1J0nhW.js → wardleyDiagram-YWT4CUSO-C4FzoV-Q.js} +1 -1
- pythinker_code/web/static/assets/{xychartDiagram-2RQKCTM6-DY3utX2b.js → xychartDiagram-2RQKCTM6-DSrsRKRD.js} +1 -1
- pythinker_code/web/static/index.html +1 -1
- {pythinker_code-0.57.0.dist-info → pythinker_code-0.59.0.dist-info}/METADATA +38 -23
- {pythinker_code-0.57.0.dist-info → pythinker_code-0.59.0.dist-info}/RECORD +119 -109
- pythinker_code/web/static/assets/architecture-7EHR7CIX-CFMeP6t3.js +0 -1
- pythinker_code/web/static/assets/channel-CIonX3Sg.js +0 -1
- pythinker_code/web/static/assets/classDiagram-4FO5ZUOK-I2Og_7hO.js +0 -1
- pythinker_code/web/static/assets/classDiagram-v2-Q7XG4LA2-I2Og_7hO.js +0 -1
- pythinker_code/web/static/assets/eventmodeling-FCH6USID-Nxb8XybE.js +0 -1
- pythinker_code/web/static/assets/mermaid-VLURNSYL-BepD7Trl.js +0 -1
- pythinker_code/web/static/assets/stateDiagram-v2-BHNVJYJU-CH7hdLfc.js +0 -1
- {pythinker_code-0.57.0.dist-info → pythinker_code-0.59.0.dist-info}/WHEEL +0 -0
- {pythinker_code-0.57.0.dist-info → pythinker_code-0.59.0.dist-info}/entry_points.txt +0 -0
- {pythinker_code-0.57.0.dist-info → pythinker_code-0.59.0.dist-info}/licenses/LICENSE +0 -0
- {pythinker_code-0.57.0.dist-info → pythinker_code-0.59.0.dist-info}/licenses/NOTICE +0 -0
pythinker_code/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,65 @@ GitHub Releases page; `0.8.0` is the new starting line.
|
|
|
15
15
|
|
|
16
16
|
## Unreleased
|
|
17
17
|
|
|
18
|
+
## 0.59.0 (2026-07-17)
|
|
19
|
+
|
|
20
|
+
- **Reviewer subagents now receive deterministic Git scopes.** Structured automatic,
|
|
21
|
+
uncommitted, base, and commit targets resolve to full commit anchors before dispatch, reject
|
|
22
|
+
invalid or empty scopes explicitly, and keep repository metadata isolated from instructions
|
|
23
|
+
across foreground and background runs.
|
|
24
|
+
- **Parallel streamed tool calls are now correlated safely.** Interleaved argument chunks stay attached to their indexed calls, malformed or truncated call streams stop before tool execution, and failed attempts are not retried after output has already been shown.
|
|
25
|
+
- **Provider compatibility and Z.AI routing are now explicit.** Immutable compatibility profiles keep request-format quirks behind the chat-provider boundary, while independent Z.AI Coding Plan and API login routes use separate credentials, endpoints, model identities, catalog refresh, logout, and usage/rate-limit state. Curated GLM requests now apply exact context/output limits, thinking controls, reasoning replay, and tool-stream support without activating for local or unknown models.
|
|
26
|
+
- **Tool execution is now supervised as a terminal batch.** A private execution engine preserves the Toolset registry and legacy per-call API while centralizing ordered results, deduplication, callbacks, and batch summaries; cancellation is bounded, late work stays owned, and new batches fail closed until timed-out cleanup drains.
|
|
27
|
+
|
|
28
|
+
Upgrade with `pythinker update`, `pip install --upgrade pythinker-code==0.59.0`, or use the native installer for your platform from the [Releases page](https://github.com/Pythoughts-labs/pythinker-code/releases/latest).
|
|
29
|
+
|
|
30
|
+
## 0.58.0 (2026-07-11)
|
|
31
|
+
|
|
32
|
+
- **Agent-spec loading is more defensive and truthful.** Subagent `path`, `extend`, and
|
|
33
|
+
`system_prompt_path` references that resolve outside their spec's directory (or the built-in
|
|
34
|
+
agents directory) are now rejected instead of loaded, and the markdown agent catalogue no longer
|
|
35
|
+
reclassifies an unexpected parser error as a harmless "invalid field" skip — only genuinely
|
|
36
|
+
malformed frontmatter is skipped.
|
|
37
|
+
- **Thinking and subagent activity now render cleanly in the terminal.** Live reasoning previews
|
|
38
|
+
render complete Markdown without exposing top-level HTML comments, activity-tree rows remain
|
|
39
|
+
visually stable, and the coral shimmer is reserved for the active verb spinner.
|
|
40
|
+
- **Agent request compatibility is now executable and reviewable.** Provider handoff,
|
|
41
|
+
prompt ordering, persisted-versus-effective history, context JSONL restoration,
|
|
42
|
+
agent projections, and Toolset lifecycle behavior now have explicit compatibility
|
|
43
|
+
contracts guarding future agent-core changes.
|
|
44
|
+
- **Skill discovery is bounded without making skills unreachable.** Pythinker now
|
|
45
|
+
searches one deterministic `SkillCatalog`, keeps exhaustive exact-name resolution,
|
|
46
|
+
and sends only task-relevant candidates to the model within an 8,000-character
|
|
47
|
+
request budget. The exhaustive `Runtime.skills` mapping remains available during
|
|
48
|
+
the compatibility window.
|
|
49
|
+
- **Agent requests now have one observable assembly path.** Required guidance fails
|
|
50
|
+
closed, optional guidance reports sanitized degradation outcomes, and the new
|
|
51
|
+
`/prompt-manifest` command explains the latest request composition without storing
|
|
52
|
+
raw prompts, user text, or provenance paths.
|
|
53
|
+
- **Conversation history updates are transactional.** Normal appends persist before
|
|
54
|
+
changing memory, while compaction, pruning, revert, and clear flows use atomic
|
|
55
|
+
replacement with coherent cancellation and rollback behavior. Concurrent revert
|
|
56
|
+
conflicts now stop after a bounded retry budget instead of starving indefinitely.
|
|
57
|
+
Existing JSONL records and restoration behavior remain compatible.
|
|
58
|
+
- **Agent definitions now resolve through one source-aware catalogue.** YAML and
|
|
59
|
+
Markdown definitions share deterministic precedence, collision diagnostics, and
|
|
60
|
+
safe provenance handling. Unknown fields warn in this release, become errors in
|
|
61
|
+
the following minor release, and the `LaborMarket`, `AgentTypeDefinition`, and
|
|
62
|
+
generated-wrapper adapters remain through that strict-default release.
|
|
63
|
+
- **Tool execution and MCP lifecycle behavior now have deterministic fault coverage.**
|
|
64
|
+
Publication rebuilds preserve the previous MCP tool registry if registration
|
|
65
|
+
fails. Characterization crossed the execution-overhead threshold, but a controlled
|
|
66
|
+
private extraction measured slightly worse and was reverted, so
|
|
67
|
+
`PythinkerToolset` remains the implementation boundary.
|
|
68
|
+
- **Agent-core seams hardened from review.** Persisted usage/checkpoint records reject
|
|
69
|
+
boolean and negative token counts, `update_token_count` validates at the boundary, a
|
|
70
|
+
temporary system-prompt descriptor is closed if `fdopen` fails, request finalization
|
|
71
|
+
surfaces every provider acknowledgement failure, a failed skill projection is always
|
|
72
|
+
recorded as failed (never blurred to not-applicable), and request-assembly telemetry no
|
|
73
|
+
longer emits unbounded per-request token values as metric attributes.
|
|
74
|
+
|
|
75
|
+
Upgrade with `pythinker update`, `pip install --upgrade pythinker-code==0.58.0`, or use the native installer for your platform from the [Releases page](https://github.com/Pythoughts-labs/pythinker-code/releases/latest).
|
|
76
|
+
|
|
18
77
|
## 0.57.0 (2026-07-05)
|
|
19
78
|
|
|
20
79
|
- **No more ghost/duplicate input prompt while the agent works.** After
|
|
@@ -255,7 +255,7 @@ Precedence per §2. `README`/`README.md` files are optional supplementary contex
|
|
|
255
255
|
|
|
256
256
|
## 12. Skills
|
|
257
257
|
|
|
258
|
-
Skills are reusable, self-contained capability directories, each with a `SKILL.md` of instructions, examples, scripts, and reference material — specialized domain knowledge, workflow patterns, pre-configured tool chains, and templates.
|
|
258
|
+
Skills are reusable, self-contained capability directories, each with a `SKILL.md` of instructions, examples, scripts, and reference material — specialized domain knowledge, workflow patterns, pre-configured tool chains, and templates. When scopes define the same name, the more specific wins: **Project › User › Extra › Built-in.**
|
|
259
259
|
|
|
260
260
|
${PYTHINKER_SKILLS}
|
|
261
261
|
|
pythinker_code/agentspec.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import hashlib
|
|
4
|
+
import re
|
|
3
5
|
from dataclasses import dataclass
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
from typing import Any, Literal, NamedTuple, cast
|
|
6
8
|
|
|
7
9
|
import yaml
|
|
8
|
-
from pydantic import BaseModel, Field
|
|
10
|
+
from pydantic import BaseModel, Field, ValidationError
|
|
9
11
|
|
|
10
12
|
from pythinker_code.exception import AgentSpecError
|
|
11
13
|
|
|
@@ -89,6 +91,14 @@ class ResolvedAgentSpec:
|
|
|
89
91
|
subagents: dict[str, SubagentSpec]
|
|
90
92
|
|
|
91
93
|
|
|
94
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
95
|
+
class AgentSpecSourceValidation:
|
|
96
|
+
"""Unknown fields observed in one canonical YAML source before projection."""
|
|
97
|
+
|
|
98
|
+
source_path: Path
|
|
99
|
+
field_paths: tuple[str, ...]
|
|
100
|
+
|
|
101
|
+
|
|
92
102
|
def load_agent_spec(agent_file: Path) -> ResolvedAgentSpec:
|
|
93
103
|
"""
|
|
94
104
|
Load agent specification from file.
|
|
@@ -97,7 +107,22 @@ def load_agent_spec(agent_file: Path) -> ResolvedAgentSpec:
|
|
|
97
107
|
FileNotFoundError: If the agent spec file is not found.
|
|
98
108
|
AgentSpecError: If the agent spec is not valid.
|
|
99
109
|
"""
|
|
100
|
-
agent_spec =
|
|
110
|
+
agent_spec, _ = load_agent_spec_validated(agent_file)
|
|
111
|
+
return agent_spec
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def load_agent_spec_validated(
|
|
115
|
+
agent_file: Path,
|
|
116
|
+
*,
|
|
117
|
+
forbid_unknown_fields: bool = False,
|
|
118
|
+
) -> tuple[ResolvedAgentSpec, tuple[AgentSpecSourceValidation, ...]]:
|
|
119
|
+
"""Load a spec and report raw unknown fields from every inherited source."""
|
|
120
|
+
validations: dict[Path, AgentSpecSourceValidation] = {}
|
|
121
|
+
agent_spec = _load_agent_spec(
|
|
122
|
+
agent_file,
|
|
123
|
+
_validations=validations,
|
|
124
|
+
_forbid_unknown_fields=forbid_unknown_fields,
|
|
125
|
+
)
|
|
101
126
|
assert agent_spec.extend is None, "agent extension should be recursively resolved"
|
|
102
127
|
if isinstance(agent_spec.name, Inherit):
|
|
103
128
|
raise AgentSpecError("Agent name is required")
|
|
@@ -111,7 +136,7 @@ def load_agent_spec(agent_file: Path) -> ResolvedAgentSpec:
|
|
|
111
136
|
agent_spec.exclude_tools = []
|
|
112
137
|
if isinstance(agent_spec.subagents, Inherit):
|
|
113
138
|
agent_spec.subagents = {}
|
|
114
|
-
|
|
139
|
+
resolved = ResolvedAgentSpec(
|
|
115
140
|
name=agent_spec.name,
|
|
116
141
|
system_prompt_path=agent_spec.system_prompt_path,
|
|
117
142
|
system_prompt_args=agent_spec.system_prompt_args,
|
|
@@ -127,9 +152,36 @@ def load_agent_spec(agent_file: Path) -> ResolvedAgentSpec:
|
|
|
127
152
|
exclude_tools=agent_spec.exclude_tools or [],
|
|
128
153
|
subagents=agent_spec.subagents or {},
|
|
129
154
|
)
|
|
155
|
+
return resolved, tuple(validations.values())
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _resolve_within_agent_roots(agent_file: Path, declared: str | Path) -> Path:
|
|
159
|
+
"""Join *declared* onto *agent_file*'s directory, rejecting escaping paths.
|
|
160
|
+
|
|
161
|
+
Trusted local agent specs reference sibling files with relative paths. A
|
|
162
|
+
``..`` traversal or symlink whose canonical target lands outside both the
|
|
163
|
+
originating spec's directory and the built-in agents directory is rejected
|
|
164
|
+
fail-closed as defense-in-depth, since the joined path is otherwise opened
|
|
165
|
+
or recursively loaded directly. The stored value keeps its ``.absolute()``
|
|
166
|
+
form, so permitted paths are unchanged.
|
|
167
|
+
"""
|
|
168
|
+
parent = agent_file.parent
|
|
169
|
+
absolute = (parent / declared).absolute()
|
|
170
|
+
allowed_roots = (parent.resolve(), get_agents_dir().resolve())
|
|
171
|
+
if not any(absolute.resolve().is_relative_to(root) for root in allowed_roots):
|
|
172
|
+
raise AgentSpecError(
|
|
173
|
+
f"Agent spec reference {declared!r} resolves outside the permitted agent directories"
|
|
174
|
+
)
|
|
175
|
+
return absolute
|
|
130
176
|
|
|
131
177
|
|
|
132
|
-
def _load_agent_spec(
|
|
178
|
+
def _load_agent_spec(
|
|
179
|
+
agent_file: Path,
|
|
180
|
+
_visited: set[Path] | None = None,
|
|
181
|
+
*,
|
|
182
|
+
_validations: dict[Path, AgentSpecSourceValidation] | None = None,
|
|
183
|
+
_forbid_unknown_fields: bool = False,
|
|
184
|
+
) -> AgentSpec:
|
|
133
185
|
resolved = agent_file.resolve()
|
|
134
186
|
if _visited is None:
|
|
135
187
|
_visited = set()
|
|
@@ -149,24 +201,46 @@ def _load_agent_spec(agent_file: Path, _visited: set[Path] | None = None) -> Age
|
|
|
149
201
|
raise AgentSpecError(f"Agent spec file must contain a mapping: {agent_file}")
|
|
150
202
|
data = cast("dict[str, Any]", data)
|
|
151
203
|
|
|
204
|
+
unknown_fields, has_invalid_field_key = _unknown_agent_spec_fields(data)
|
|
205
|
+
if unknown_fields:
|
|
206
|
+
if has_invalid_field_key:
|
|
207
|
+
fields = ", ".join(unknown_fields)
|
|
208
|
+
raise AgentSpecError(f"Invalid agent field key: {fields}")
|
|
209
|
+
if _forbid_unknown_fields:
|
|
210
|
+
fields = ", ".join(unknown_fields)
|
|
211
|
+
raise AgentSpecError(f"Unknown fields in required agent source: {fields}")
|
|
212
|
+
if _validations is not None and resolved not in _validations:
|
|
213
|
+
_validations[resolved] = AgentSpecSourceValidation(
|
|
214
|
+
source_path=resolved,
|
|
215
|
+
field_paths=unknown_fields,
|
|
216
|
+
)
|
|
217
|
+
|
|
152
218
|
version = str(data.get("version", DEFAULT_AGENT_SPEC_VERSION))
|
|
153
219
|
if version not in SUPPORTED_AGENT_SPEC_VERSIONS:
|
|
154
220
|
raise AgentSpecError(f"Unsupported agent spec version: {version}")
|
|
155
221
|
|
|
156
|
-
|
|
222
|
+
try:
|
|
223
|
+
agent_spec = AgentSpec(**data.get("agent", {}))
|
|
224
|
+
except (TypeError, ValidationError) as exc:
|
|
225
|
+
raise AgentSpecError("Agent spec contains an invalid known field") from exc
|
|
157
226
|
if isinstance(agent_spec.system_prompt_path, Path):
|
|
158
|
-
agent_spec.system_prompt_path = (
|
|
159
|
-
agent_file
|
|
160
|
-
)
|
|
227
|
+
agent_spec.system_prompt_path = _resolve_within_agent_roots(
|
|
228
|
+
agent_file, agent_spec.system_prompt_path
|
|
229
|
+
)
|
|
161
230
|
if isinstance(agent_spec.subagents, dict):
|
|
162
231
|
for v in agent_spec.subagents.values():
|
|
163
|
-
v.path = (agent_file
|
|
232
|
+
v.path = _resolve_within_agent_roots(agent_file, v.path)
|
|
164
233
|
if agent_spec.extend:
|
|
165
234
|
if agent_spec.extend == "default":
|
|
166
235
|
base_agent_file = DEFAULT_AGENT_FILE
|
|
167
236
|
else:
|
|
168
|
-
base_agent_file = (agent_file
|
|
169
|
-
base_agent_spec = _load_agent_spec(
|
|
237
|
+
base_agent_file = _resolve_within_agent_roots(agent_file, agent_spec.extend)
|
|
238
|
+
base_agent_spec = _load_agent_spec(
|
|
239
|
+
base_agent_file,
|
|
240
|
+
_visited,
|
|
241
|
+
_validations=_validations,
|
|
242
|
+
_forbid_unknown_fields=_forbid_unknown_fields,
|
|
243
|
+
)
|
|
170
244
|
if not isinstance(agent_spec.name, Inherit):
|
|
171
245
|
base_agent_spec.name = agent_spec.name
|
|
172
246
|
if not isinstance(agent_spec.system_prompt_path, Inherit):
|
|
@@ -207,3 +281,89 @@ def _load_agent_spec(agent_file: Path, _visited: set[Path] | None = None) -> Age
|
|
|
207
281
|
base_agent_spec.subagents = agent_spec.subagents
|
|
208
282
|
agent_spec = base_agent_spec
|
|
209
283
|
return agent_spec
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _unknown_agent_spec_fields(data: dict[str, Any]) -> tuple[tuple[str, ...], bool]:
|
|
287
|
+
unknown: list[str] = []
|
|
288
|
+
has_invalid_key = False
|
|
289
|
+
for key in cast("dict[object, object]", data):
|
|
290
|
+
if isinstance(key, str) and key in {"version", "agent"}:
|
|
291
|
+
continue
|
|
292
|
+
rendered = render_agent_field_segment(key)
|
|
293
|
+
unknown.append(rendered.text)
|
|
294
|
+
has_invalid_key = has_invalid_key or rendered.structurally_invalid
|
|
295
|
+
raw_agent = data.get("agent")
|
|
296
|
+
if not isinstance(raw_agent, dict):
|
|
297
|
+
return tuple(sorted(unknown)), has_invalid_key
|
|
298
|
+
agent = cast("dict[str, Any]", raw_agent)
|
|
299
|
+
known_agent_fields = set(AgentSpec.model_fields)
|
|
300
|
+
for key in cast("dict[object, object]", agent):
|
|
301
|
+
if isinstance(key, str) and key in known_agent_fields:
|
|
302
|
+
continue
|
|
303
|
+
rendered = render_agent_field_segment(key)
|
|
304
|
+
unknown.append(f"agent.{rendered.text}")
|
|
305
|
+
has_invalid_key = has_invalid_key or rendered.structurally_invalid
|
|
306
|
+
raw_subagents = agent.get("subagents")
|
|
307
|
+
if isinstance(raw_subagents, dict):
|
|
308
|
+
known_subagent_fields = set(SubagentSpec.model_fields)
|
|
309
|
+
for name, raw_subagent in cast("dict[object, object]", raw_subagents).items():
|
|
310
|
+
rendered_name = render_agent_field_segment(name)
|
|
311
|
+
has_invalid_key = has_invalid_key or rendered_name.structurally_invalid
|
|
312
|
+
if rendered_name.structurally_invalid:
|
|
313
|
+
unknown.append(f"agent.subagents.{rendered_name.text}")
|
|
314
|
+
if not isinstance(raw_subagent, dict):
|
|
315
|
+
continue
|
|
316
|
+
for key in cast("dict[object, object]", raw_subagent):
|
|
317
|
+
if isinstance(key, str) and key in known_subagent_fields:
|
|
318
|
+
continue
|
|
319
|
+
rendered = render_agent_field_segment(key)
|
|
320
|
+
unknown.append(f"agent.subagents.{rendered_name.text}.{rendered.text}")
|
|
321
|
+
has_invalid_key = has_invalid_key or rendered.structurally_invalid
|
|
322
|
+
return tuple(sorted(unknown)), has_invalid_key
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
_FIELD_IDENTIFIER_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_-]*")
|
|
326
|
+
_SENSITIVE_FIELD_HINTS = (
|
|
327
|
+
"password",
|
|
328
|
+
"passwd",
|
|
329
|
+
"secret",
|
|
330
|
+
"token",
|
|
331
|
+
"api_key",
|
|
332
|
+
"apikey",
|
|
333
|
+
"access_key",
|
|
334
|
+
"private_key",
|
|
335
|
+
"credential",
|
|
336
|
+
"auth",
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
341
|
+
class AgentFieldSegment:
|
|
342
|
+
text: str
|
|
343
|
+
redacted_for_safety: bool
|
|
344
|
+
structurally_invalid: bool
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def render_agent_field_segment(value: object) -> AgentFieldSegment:
|
|
348
|
+
"""Render a stable field segment without conflating redaction and validity."""
|
|
349
|
+
if isinstance(value, str):
|
|
350
|
+
lowered = value.casefold()
|
|
351
|
+
if _FIELD_IDENTIFIER_RE.fullmatch(value) and not any(
|
|
352
|
+
hint in lowered for hint in _SENSITIVE_FIELD_HINTS
|
|
353
|
+
):
|
|
354
|
+
return AgentFieldSegment(
|
|
355
|
+
text=value,
|
|
356
|
+
redacted_for_safety=False,
|
|
357
|
+
structurally_invalid=False,
|
|
358
|
+
)
|
|
359
|
+
digest_input = f"str:{value}"
|
|
360
|
+
structurally_invalid = _FIELD_IDENTIFIER_RE.fullmatch(value) is None
|
|
361
|
+
else:
|
|
362
|
+
digest_input = f"{type(value).__qualname__}:{value!r}"
|
|
363
|
+
structurally_invalid = True
|
|
364
|
+
digest = hashlib.sha256(digest_input.encode(encoding="utf-8")).hexdigest()[:12]
|
|
365
|
+
return AgentFieldSegment(
|
|
366
|
+
text=f"field[{digest}]",
|
|
367
|
+
redacted_for_safety=True,
|
|
368
|
+
structurally_invalid=structurally_invalid,
|
|
369
|
+
)
|
pythinker_code/auth/__init__.py
CHANGED
|
@@ -13,7 +13,8 @@ ANTHROPIC_PLATFORM_ID = "anthropic"
|
|
|
13
13
|
OPENROUTER_PLATFORM_ID = "openrouter"
|
|
14
14
|
LM_STUDIO_PLATFORM_ID = "lm-studio"
|
|
15
15
|
OLLAMA_PLATFORM_ID = "ollama"
|
|
16
|
-
|
|
16
|
+
ZAI_CODING_PLATFORM_ID = "z-ai-coding"
|
|
17
|
+
ZAI_API_PLATFORM_ID = "z-ai-api"
|
|
17
18
|
|
|
18
19
|
__all__ = [
|
|
19
20
|
"ALIBABA_PLATFORM_ID",
|
|
@@ -29,5 +30,6 @@ __all__ = [
|
|
|
29
30
|
"OPENCODE_GO_PLATFORM_ID",
|
|
30
31
|
"OPENROUTER_PLATFORM_ID",
|
|
31
32
|
"PYTHINKER_CODE_PLATFORM_ID",
|
|
32
|
-
"
|
|
33
|
+
"ZAI_API_PLATFORM_ID",
|
|
34
|
+
"ZAI_CODING_PLATFORM_ID",
|
|
33
35
|
]
|
pythinker_code/auth/platforms.py
CHANGED
|
@@ -250,8 +250,9 @@ async def refresh_managed_models(config: Config) -> bool:
|
|
|
250
250
|
refresh_opencode_go_models,
|
|
251
251
|
)
|
|
252
252
|
from pythinker_code.auth.z_ai import (
|
|
253
|
-
|
|
254
|
-
|
|
253
|
+
ZAI_ROUTES,
|
|
254
|
+
ZaiCatalogResult,
|
|
255
|
+
ZaiRoute,
|
|
255
256
|
apply_z_ai_models,
|
|
256
257
|
refresh_z_ai_models,
|
|
257
258
|
)
|
|
@@ -264,16 +265,17 @@ async def refresh_managed_models(config: Config) -> bool:
|
|
|
264
265
|
|
|
265
266
|
changed = False
|
|
266
267
|
updates: list[tuple[str, str, list[ModelInfo]]] = []
|
|
268
|
+
z_ai_provider_keys = {route.provider_key for route in ZAI_ROUTES}
|
|
267
269
|
oauth_manager = None
|
|
268
270
|
for provider_key, provider in managed_providers.items():
|
|
269
271
|
# OpenCode Go and MiniMax own provider-specific model discovery. The
|
|
270
272
|
# generic `managed:<platform>` path can't express OpenCode Go's
|
|
271
273
|
# two-provider split, and MiniMax's provider key intentionally includes
|
|
272
274
|
# the wire-shape suffix (`managed:minimax-anthropic`).
|
|
273
|
-
if
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
275
|
+
if (
|
|
276
|
+
provider_key in OPENCODE_GO_PROVIDER_KEYS
|
|
277
|
+
or provider_key in {MINIMAX_ANTHROPIC_PROVIDER_KEY, KIMI_PROVIDER_KEY}
|
|
278
|
+
or provider_key in z_ai_provider_keys
|
|
277
279
|
):
|
|
278
280
|
continue
|
|
279
281
|
platform_id = parse_managed_provider_key(provider_key)
|
|
@@ -430,13 +432,29 @@ async def refresh_managed_models(config: Config) -> bool:
|
|
|
430
432
|
if minimax_models is not None and apply_minimax_models(config, minimax_models):
|
|
431
433
|
changed = True
|
|
432
434
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
435
|
+
z_ai_results: dict[ZaiRoute, ZaiCatalogResult] = {}
|
|
436
|
+
for route in ZAI_ROUTES:
|
|
437
|
+
try:
|
|
438
|
+
result = await refresh_z_ai_models(config, route)
|
|
439
|
+
except Exception as exc:
|
|
440
|
+
logger.warning(
|
|
441
|
+
"Unexpected Z.AI catalog refresh failure for {route}: {error_type}",
|
|
442
|
+
route=route.platform_id,
|
|
443
|
+
error_type=type(exc).__name__,
|
|
444
|
+
)
|
|
445
|
+
continue
|
|
446
|
+
z_ai_results[route] = result
|
|
447
|
+
if result.status == "live":
|
|
448
|
+
assert result.models is not None
|
|
449
|
+
if apply_z_ai_models(config, route, result.models):
|
|
450
|
+
changed = True
|
|
451
|
+
elif result.status != "unconfigured":
|
|
452
|
+
logger.warning(
|
|
453
|
+
"Z.AI catalog refresh did not update {route}: status={status}, failure={failure}",
|
|
454
|
+
route=route.platform_id,
|
|
455
|
+
status=result.status,
|
|
456
|
+
failure=result.failure,
|
|
457
|
+
)
|
|
440
458
|
|
|
441
459
|
kimi_models: tuple[KimiModel, ...] | None = None
|
|
442
460
|
try:
|
|
@@ -456,8 +474,12 @@ async def refresh_managed_models(config: Config) -> bool:
|
|
|
456
474
|
save_changed = True
|
|
457
475
|
if minimax_models is not None and apply_minimax_models(config_for_save, minimax_models):
|
|
458
476
|
save_changed = True
|
|
459
|
-
|
|
460
|
-
|
|
477
|
+
for route, result in z_ai_results.items():
|
|
478
|
+
if result.status != "live":
|
|
479
|
+
continue
|
|
480
|
+
assert result.models is not None
|
|
481
|
+
if apply_z_ai_models(config_for_save, route, result.models):
|
|
482
|
+
save_changed = True
|
|
461
483
|
if kimi_models is not None and apply_kimi_models(config_for_save, kimi_models):
|
|
462
484
|
save_changed = True
|
|
463
485
|
if save_changed:
|