forge-cpp-mcp 0.2.1__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.
- forge_cpp_mcp-0.2.1.dist-info/METADATA +436 -0
- forge_cpp_mcp-0.2.1.dist-info/RECORD +83 -0
- forge_cpp_mcp-0.2.1.dist-info/WHEEL +4 -0
- forge_cpp_mcp-0.2.1.dist-info/entry_points.txt +2 -0
- forge_cpp_mcp-0.2.1.dist-info/licenses/LICENSE +21 -0
- forgemcp/__init__.py +5 -0
- forgemcp/assets/clangd-result.html +119 -0
- forgemcp/assets/cmake-build.html +111 -0
- forgemcp/assets/cmake-configure.html +111 -0
- forgemcp/assets/cmake-profiles.html +111 -0
- forgemcp/assets/cmake-test.html +111 -0
- forgemcp/assets/process-details.html +113 -0
- forgemcp/assets/process-overview.html +111 -0
- forgemcp/assets/toolsets.html +111 -0
- forgemcp/assets/workspace-file.html +119 -0
- forgemcp/assets/workspace-result.html +119 -0
- forgemcp/assets/workspace-search.html +119 -0
- forgemcp/assets/workspace-tree.html +119 -0
- forgemcp/assets.py +62 -0
- forgemcp/clangd/__init__.py +1 -0
- forgemcp/clangd/errors.py +29 -0
- forgemcp/clangd/models.py +94 -0
- forgemcp/clangd/service.py +1279 -0
- forgemcp/clangd/session.py +520 -0
- forgemcp/cmake/__init__.py +1 -0
- forgemcp/cmake/errors.py +5 -0
- forgemcp/cmake/profiles.py +95 -0
- forgemcp/cmake/service.py +895 -0
- forgemcp/completion.py +49 -0
- forgemcp/icons/clangd.LICENSE +219 -0
- forgemcp/icons/clangd.svg +25 -0
- forgemcp/icons/cmake.svg +9 -0
- forgemcp/icons/process.svg +4 -0
- forgemcp/icons/toolchain.svg +1 -0
- forgemcp/icons/workspace-edit.svg +1 -0
- forgemcp/icons/workspace-file.svg +1 -0
- forgemcp/icons/workspace-search.svg +1 -0
- forgemcp/icons/workspace.svg +4 -0
- forgemcp/markdown.py +308 -0
- forgemcp/process/__init__.py +21 -0
- forgemcp/process/errors.py +21 -0
- forgemcp/process/models.py +171 -0
- forgemcp/process/service.py +830 -0
- forgemcp/process/transcript.py +116 -0
- forgemcp/progress.py +40 -0
- forgemcp/server.py +196 -0
- forgemcp/toolchain/__init__.py +1 -0
- forgemcp/toolchain/discovery.py +25 -0
- forgemcp/toolchain/errors.py +41 -0
- forgemcp/toolchain/loader.py +48 -0
- forgemcp/toolchain/providers/__init__.py +1 -0
- forgemcp/toolchain/providers/system.py +52 -0
- forgemcp/toolchain/providers/user.py +93 -0
- forgemcp/toolchain/providers/visual_studio.py +202 -0
- forgemcp/toolchain/service.py +226 -0
- forgemcp/toolchain/spec.py +73 -0
- forgemcp/toolchain/tools/__init__.py +1 -0
- forgemcp/toolchain/tools/clang.py +61 -0
- forgemcp/toolchain/tools/clang_cl.py +61 -0
- forgemcp/toolchain/tools/clangd.py +280 -0
- forgemcp/toolchain/tools/clangxx.py +61 -0
- forgemcp/toolchain/tools/cmake.py +280 -0
- forgemcp/toolchain/tools/cppvsdbg.py +27 -0
- forgemcp/toolchain/tools/ctest.py +202 -0
- forgemcp/toolchain/tools/gcc.py +61 -0
- forgemcp/toolchain/tools/gdb.py +61 -0
- forgemcp/toolchain/tools/git.py +61 -0
- forgemcp/toolchain/tools/gxx.py +61 -0
- forgemcp/toolchain/tools/link.py +27 -0
- forgemcp/toolchain/tools/lld.py +61 -0
- forgemcp/toolchain/tools/lld_link.py +61 -0
- forgemcp/toolchain/tools/lldb_dap.py +27 -0
- forgemcp/toolchain/tools/make.py +61 -0
- forgemcp/toolchain/tools/msbuild.py +61 -0
- forgemcp/toolchain/tools/msvc.py +27 -0
- forgemcp/toolchain/tools/ninja.py +61 -0
- forgemcp/workspace/__init__.py +5 -0
- forgemcp/workspace/diff.py +181 -0
- forgemcp/workspace/errors.py +5 -0
- forgemcp/workspace/metadata.py +87 -0
- forgemcp/workspace/path.py +71 -0
- forgemcp/workspace/providers.py +25 -0
- forgemcp/workspace/service.py +1766 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Typed methods for msbuild."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TypedDict
|
|
7
|
+
|
|
8
|
+
from forgemcp.process.errors import ProcessError
|
|
9
|
+
from forgemcp.process.models import ProcessTimeout
|
|
10
|
+
from forgemcp.process.service import ProcessService
|
|
11
|
+
|
|
12
|
+
from ..errors import ToolCommandError, ToolParserError
|
|
13
|
+
from ..spec import ToolInfo, ToolKind, ToolSpec
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Methods(TypedDict):
|
|
17
|
+
version: Callable[[], Awaitable[str]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_spec(
|
|
21
|
+
path: Path,
|
|
22
|
+
processes: ProcessService,
|
|
23
|
+
environment: Mapping[str, str] | None = None,
|
|
24
|
+
inherit_environment: bool = True,
|
|
25
|
+
) -> ToolSpec:
|
|
26
|
+
path = path.resolve()
|
|
27
|
+
|
|
28
|
+
async def version() -> str:
|
|
29
|
+
output = {"stdout": "", "stderr": ""}
|
|
30
|
+
try:
|
|
31
|
+
async with await processes.launch(
|
|
32
|
+
path,
|
|
33
|
+
('-version', '-nologo'),
|
|
34
|
+
env=environment,
|
|
35
|
+
inherit_environment=inherit_environment,
|
|
36
|
+
timeout=ProcessTimeout(total=15),
|
|
37
|
+
) as session:
|
|
38
|
+
await session.close_stdin()
|
|
39
|
+
async for chunk in session.output():
|
|
40
|
+
# Only the short version banner is needed; drain the rest.
|
|
41
|
+
remaining = 4096 - len(output[chunk.stream])
|
|
42
|
+
output[chunk.stream] += chunk.text[:remaining]
|
|
43
|
+
code = await session.wait()
|
|
44
|
+
if code != 0:
|
|
45
|
+
raise ToolCommandError(f"{INFO.name}.version failed (exit {code}).")
|
|
46
|
+
except ProcessError as error:
|
|
47
|
+
raise ToolCommandError(f"Cannot read {INFO.name} version.") from error
|
|
48
|
+
for text in output.values():
|
|
49
|
+
if match := re.search(
|
|
50
|
+
'^([0-9]+\\.[0-9]+[^\\s]*)',
|
|
51
|
+
text,
|
|
52
|
+
re.IGNORECASE | re.MULTILINE,
|
|
53
|
+
):
|
|
54
|
+
return match.group(1)
|
|
55
|
+
raise ToolParserError(f"Cannot parse {INFO.name} version.")
|
|
56
|
+
|
|
57
|
+
methods: Methods = {"version": version}
|
|
58
|
+
return ToolSpec(INFO.name, INFO.kind, path, methods)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
INFO = ToolInfo(name='msbuild', kind=ToolKind.BUILD_RUNNER, create_spec=create_spec)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Typed methods for cl."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TypedDict
|
|
6
|
+
|
|
7
|
+
from forgemcp.process.service import ProcessService
|
|
8
|
+
|
|
9
|
+
from ..spec import ToolInfo, ToolKind, ToolSpec
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Methods(TypedDict):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_spec(
|
|
17
|
+
path: Path,
|
|
18
|
+
processes: ProcessService,
|
|
19
|
+
environment: Mapping[str, str] | None = None,
|
|
20
|
+
inherit_environment: bool = True,
|
|
21
|
+
) -> ToolSpec:
|
|
22
|
+
path = path.resolve()
|
|
23
|
+
methods: Methods = {}
|
|
24
|
+
return ToolSpec(INFO.name, INFO.kind, path, methods)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
INFO = ToolInfo(name='cl', kind=ToolKind.COMPILER, create_spec=create_spec)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Typed methods for ninja."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TypedDict
|
|
7
|
+
|
|
8
|
+
from forgemcp.process.errors import ProcessError
|
|
9
|
+
from forgemcp.process.models import ProcessTimeout
|
|
10
|
+
from forgemcp.process.service import ProcessService
|
|
11
|
+
|
|
12
|
+
from ..errors import ToolCommandError, ToolParserError
|
|
13
|
+
from ..spec import ToolInfo, ToolKind, ToolSpec
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Methods(TypedDict):
|
|
17
|
+
version: Callable[[], Awaitable[str]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_spec(
|
|
21
|
+
path: Path,
|
|
22
|
+
processes: ProcessService,
|
|
23
|
+
environment: Mapping[str, str] | None = None,
|
|
24
|
+
inherit_environment: bool = True,
|
|
25
|
+
) -> ToolSpec:
|
|
26
|
+
path = path.resolve()
|
|
27
|
+
|
|
28
|
+
async def version() -> str:
|
|
29
|
+
output = {"stdout": "", "stderr": ""}
|
|
30
|
+
try:
|
|
31
|
+
async with await processes.launch(
|
|
32
|
+
path,
|
|
33
|
+
('--version',),
|
|
34
|
+
env=environment,
|
|
35
|
+
inherit_environment=inherit_environment,
|
|
36
|
+
timeout=ProcessTimeout(total=15),
|
|
37
|
+
) as session:
|
|
38
|
+
await session.close_stdin()
|
|
39
|
+
async for chunk in session.output():
|
|
40
|
+
# Only the short version banner is needed; drain the rest.
|
|
41
|
+
remaining = 4096 - len(output[chunk.stream])
|
|
42
|
+
output[chunk.stream] += chunk.text[:remaining]
|
|
43
|
+
code = await session.wait()
|
|
44
|
+
if code != 0:
|
|
45
|
+
raise ToolCommandError(f"{INFO.name}.version failed (exit {code}).")
|
|
46
|
+
except ProcessError as error:
|
|
47
|
+
raise ToolCommandError(f"Cannot read {INFO.name} version.") from error
|
|
48
|
+
for text in output.values():
|
|
49
|
+
if match := re.search(
|
|
50
|
+
'^([0-9]+\\.[0-9]+[^\\s]*)',
|
|
51
|
+
text,
|
|
52
|
+
re.IGNORECASE | re.MULTILINE,
|
|
53
|
+
):
|
|
54
|
+
return match.group(1)
|
|
55
|
+
raise ToolParserError(f"Cannot parse {INFO.name} version.")
|
|
56
|
+
|
|
57
|
+
methods: Methods = {"version": version}
|
|
58
|
+
return ToolSpec(INFO.name, INFO.kind, path, methods)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
INFO = ToolInfo(name='ninja', kind=ToolKind.BUILD_RUNNER, create_spec=create_spec)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Typed, immutable line diffs for workspace mutation results."""
|
|
2
|
+
|
|
3
|
+
import difflib
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from .path import WorkspacePath
|
|
9
|
+
from .service import (
|
|
10
|
+
FileEditResult,
|
|
11
|
+
FileWriteResult,
|
|
12
|
+
ResultProvider,
|
|
13
|
+
WorkspaceService,
|
|
14
|
+
read_text,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DiffLine(BaseModel):
|
|
19
|
+
kind: Literal["context", "added", "removed"]
|
|
20
|
+
before_line: int | None = Field(ge=1)
|
|
21
|
+
after_line: int | None = Field(ge=1)
|
|
22
|
+
text: str
|
|
23
|
+
spans: list[tuple[int, int]] = Field(default_factory=list)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DiffRange(BaseModel):
|
|
27
|
+
kind: Literal["replace", "insert", "delete"]
|
|
28
|
+
before_start: int = Field(ge=1)
|
|
29
|
+
before_count: int = Field(ge=0)
|
|
30
|
+
after_start: int = Field(ge=1)
|
|
31
|
+
after_count: int = Field(ge=0)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DiffHunk(BaseModel):
|
|
35
|
+
before_start: int = Field(ge=1)
|
|
36
|
+
before_count: int = Field(ge=0)
|
|
37
|
+
after_start: int = Field(ge=1)
|
|
38
|
+
after_count: int = Field(ge=0)
|
|
39
|
+
lines: list[DiffLine]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class FileDiff(BaseModel):
|
|
43
|
+
path: WorkspacePath
|
|
44
|
+
changes: list[DiffRange]
|
|
45
|
+
hunks: list[DiffHunk]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def create_diff(path: WorkspacePath, before_text: str, after_text: str) -> FileDiff:
|
|
49
|
+
"""Preserve exact line text, including line endings and a missing final newline."""
|
|
50
|
+
before = before_text.splitlines(keepends=True)
|
|
51
|
+
after = after_text.splitlines(keepends=True)
|
|
52
|
+
matcher = difflib.SequenceMatcher(
|
|
53
|
+
None,
|
|
54
|
+
before,
|
|
55
|
+
after,
|
|
56
|
+
autojunk=False,
|
|
57
|
+
)
|
|
58
|
+
changes = [
|
|
59
|
+
DiffRange(
|
|
60
|
+
kind=kind,
|
|
61
|
+
before_start=i + 1,
|
|
62
|
+
before_count=j - i,
|
|
63
|
+
after_start=k + 1,
|
|
64
|
+
after_count=l - k,
|
|
65
|
+
)
|
|
66
|
+
for kind, i, j, k, l in matcher.get_opcodes()
|
|
67
|
+
if kind != "equal"
|
|
68
|
+
]
|
|
69
|
+
hunks = []
|
|
70
|
+
for group in matcher.get_grouped_opcodes(3):
|
|
71
|
+
lines = []
|
|
72
|
+
for kind, i, j, k, l in group:
|
|
73
|
+
if kind == "equal":
|
|
74
|
+
lines.extend(
|
|
75
|
+
DiffLine(
|
|
76
|
+
kind="context",
|
|
77
|
+
before_line=number + 1,
|
|
78
|
+
after_line=k + number - i + 1,
|
|
79
|
+
text=before[number],
|
|
80
|
+
)
|
|
81
|
+
for number in range(i, j)
|
|
82
|
+
)
|
|
83
|
+
else:
|
|
84
|
+
before_spans = {}
|
|
85
|
+
after_spans = {}
|
|
86
|
+
if kind == "replace":
|
|
87
|
+
for old, new in zip(range(i, j), range(k, l)):
|
|
88
|
+
character_diff = difflib.SequenceMatcher(
|
|
89
|
+
None,
|
|
90
|
+
before[old],
|
|
91
|
+
after[new],
|
|
92
|
+
autojunk=False,
|
|
93
|
+
)
|
|
94
|
+
before_spans[old] = []
|
|
95
|
+
after_spans[new] = []
|
|
96
|
+
for operation, a, b, c, d in character_diff.get_opcodes():
|
|
97
|
+
if operation != "equal":
|
|
98
|
+
before_spans[old].append((a, b))
|
|
99
|
+
after_spans[new].append((c, d))
|
|
100
|
+
lines.extend(
|
|
101
|
+
DiffLine(
|
|
102
|
+
kind="removed",
|
|
103
|
+
before_line=number + 1,
|
|
104
|
+
after_line=None,
|
|
105
|
+
text=before[number],
|
|
106
|
+
spans=before_spans.get(number, [(0, len(before[number]))]),
|
|
107
|
+
)
|
|
108
|
+
for number in range(i, j)
|
|
109
|
+
)
|
|
110
|
+
lines.extend(
|
|
111
|
+
DiffLine(
|
|
112
|
+
kind="added",
|
|
113
|
+
before_line=None,
|
|
114
|
+
after_line=number + 1,
|
|
115
|
+
text=after[number],
|
|
116
|
+
spans=after_spans.get(number, [(0, len(after[number]))]),
|
|
117
|
+
)
|
|
118
|
+
for number in range(k, l)
|
|
119
|
+
)
|
|
120
|
+
hunks.append(
|
|
121
|
+
DiffHunk(
|
|
122
|
+
before_start=group[0][1] + 1,
|
|
123
|
+
before_count=group[-1][2] - group[0][1],
|
|
124
|
+
after_start=group[0][3] + 1,
|
|
125
|
+
after_count=group[-1][4] - group[0][3],
|
|
126
|
+
lines=lines,
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
return FileDiff(path=path, changes=changes, hunks=hunks)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class DiffProvider(ResultProvider[str]):
|
|
133
|
+
"""Capture the old text before a write/edit and publish its immutable diff."""
|
|
134
|
+
|
|
135
|
+
def __init__(self, workspace: WorkspaceService) -> None:
|
|
136
|
+
self.workspace = workspace
|
|
137
|
+
|
|
138
|
+
async def before_workspace_write_file(
|
|
139
|
+
self,
|
|
140
|
+
call_id: str,
|
|
141
|
+
path: WorkspacePath,
|
|
142
|
+
text: str,
|
|
143
|
+
) -> str:
|
|
144
|
+
candidate = self.workspace.resolve_workspace_path(path)
|
|
145
|
+
return read_text(candidate, path) if candidate.exists() else ""
|
|
146
|
+
|
|
147
|
+
async def after_workspace_write_file(
|
|
148
|
+
self,
|
|
149
|
+
call_id: str,
|
|
150
|
+
context: str,
|
|
151
|
+
result: FileWriteResult,
|
|
152
|
+
) -> str:
|
|
153
|
+
return self.save_diff(call_id, result.path, context)
|
|
154
|
+
|
|
155
|
+
async def before_workspace_edit_file(
|
|
156
|
+
self,
|
|
157
|
+
call_id: str,
|
|
158
|
+
path: WorkspacePath,
|
|
159
|
+
old_text: str,
|
|
160
|
+
new_text: str,
|
|
161
|
+
replace_all: bool,
|
|
162
|
+
) -> str:
|
|
163
|
+
return read_text(self.workspace.resolve_workspace_path(path), path)
|
|
164
|
+
|
|
165
|
+
async def after_workspace_edit_file(
|
|
166
|
+
self,
|
|
167
|
+
call_id: str,
|
|
168
|
+
context: str,
|
|
169
|
+
result: FileEditResult,
|
|
170
|
+
) -> str:
|
|
171
|
+
return self.save_diff(call_id, result.path, context)
|
|
172
|
+
|
|
173
|
+
def save_diff(self, call_id: str, path: WorkspacePath, before: str) -> str:
|
|
174
|
+
after = read_text(self.workspace.resolve_workspace_path(path), path)
|
|
175
|
+
diff = create_diff(path, before, after)
|
|
176
|
+
return self.workspace.save_result_resource(
|
|
177
|
+
call_id,
|
|
178
|
+
"diff",
|
|
179
|
+
"application/json",
|
|
180
|
+
diff.model_dump_json(),
|
|
181
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Platform-specific file ownership; unavailable accounts are represented by None."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def file_owner(path: Path) -> str | None:
|
|
8
|
+
if os.name != "nt":
|
|
9
|
+
try:
|
|
10
|
+
return path.owner()
|
|
11
|
+
except (KeyError, OSError, NotImplementedError):
|
|
12
|
+
return None
|
|
13
|
+
|
|
14
|
+
import ctypes
|
|
15
|
+
from ctypes import wintypes
|
|
16
|
+
|
|
17
|
+
advapi = ctypes.WinDLL("advapi32", use_last_error=True)
|
|
18
|
+
kernel = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
19
|
+
owner, descriptor = ctypes.c_void_p(), ctypes.c_void_p()
|
|
20
|
+
get_security = advapi.GetNamedSecurityInfoW
|
|
21
|
+
get_security.argtypes = [
|
|
22
|
+
wintypes.LPWSTR,
|
|
23
|
+
wintypes.DWORD,
|
|
24
|
+
wintypes.DWORD,
|
|
25
|
+
ctypes.POINTER(ctypes.c_void_p),
|
|
26
|
+
ctypes.c_void_p,
|
|
27
|
+
ctypes.c_void_p,
|
|
28
|
+
ctypes.c_void_p,
|
|
29
|
+
ctypes.POINTER(ctypes.c_void_p),
|
|
30
|
+
]
|
|
31
|
+
get_security.restype = wintypes.DWORD
|
|
32
|
+
lookup = advapi.LookupAccountSidW
|
|
33
|
+
lookup.argtypes = [
|
|
34
|
+
wintypes.LPCWSTR,
|
|
35
|
+
ctypes.c_void_p,
|
|
36
|
+
wintypes.LPWSTR,
|
|
37
|
+
ctypes.POINTER(wintypes.DWORD),
|
|
38
|
+
wintypes.LPWSTR,
|
|
39
|
+
ctypes.POINTER(wintypes.DWORD),
|
|
40
|
+
ctypes.POINTER(wintypes.DWORD),
|
|
41
|
+
]
|
|
42
|
+
lookup.restype = wintypes.BOOL
|
|
43
|
+
kernel.LocalFree.argtypes = [ctypes.c_void_p]
|
|
44
|
+
kernel.LocalFree.restype = ctypes.c_void_p
|
|
45
|
+
if get_security(
|
|
46
|
+
str(path),
|
|
47
|
+
1,
|
|
48
|
+
1,
|
|
49
|
+
ctypes.byref(owner),
|
|
50
|
+
None,
|
|
51
|
+
None,
|
|
52
|
+
None,
|
|
53
|
+
ctypes.byref(descriptor),
|
|
54
|
+
):
|
|
55
|
+
return None
|
|
56
|
+
try:
|
|
57
|
+
name_size, domain_size, kind = (
|
|
58
|
+
wintypes.DWORD(),
|
|
59
|
+
wintypes.DWORD(),
|
|
60
|
+
wintypes.DWORD(),
|
|
61
|
+
)
|
|
62
|
+
lookup(
|
|
63
|
+
None,
|
|
64
|
+
owner,
|
|
65
|
+
None,
|
|
66
|
+
ctypes.byref(name_size),
|
|
67
|
+
None,
|
|
68
|
+
ctypes.byref(domain_size),
|
|
69
|
+
ctypes.byref(kind),
|
|
70
|
+
)
|
|
71
|
+
if not name_size.value:
|
|
72
|
+
return None
|
|
73
|
+
name = ctypes.create_unicode_buffer(name_size.value)
|
|
74
|
+
domain = ctypes.create_unicode_buffer(max(1, domain_size.value))
|
|
75
|
+
if not lookup(
|
|
76
|
+
None,
|
|
77
|
+
owner,
|
|
78
|
+
name,
|
|
79
|
+
ctypes.byref(name_size),
|
|
80
|
+
domain,
|
|
81
|
+
ctypes.byref(domain_size),
|
|
82
|
+
ctypes.byref(kind),
|
|
83
|
+
):
|
|
84
|
+
return None
|
|
85
|
+
return f"{domain.value}\\{name.value}" if domain.value else name.value
|
|
86
|
+
finally:
|
|
87
|
+
kernel.LocalFree(descriptor)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Qualified managed paths and explicit external locations shared by modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path, PurePosixPath, PureWindowsPath
|
|
6
|
+
from typing import Literal, cast
|
|
7
|
+
|
|
8
|
+
from pydantic import GetCoreSchemaHandler
|
|
9
|
+
from pydantic_core import CoreSchema, core_schema
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WorkspacePath(str):
|
|
13
|
+
"""A project/storage path, or root/ followed by an absolute native path."""
|
|
14
|
+
|
|
15
|
+
def __new__(cls, value: str) -> WorkspacePath:
|
|
16
|
+
instance = super().__new__(cls, value)
|
|
17
|
+
instance.validate_path()
|
|
18
|
+
return instance
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def __get_pydantic_core_schema__(
|
|
22
|
+
cls,
|
|
23
|
+
source_type: object,
|
|
24
|
+
handler: GetCoreSchemaHandler,
|
|
25
|
+
) -> CoreSchema:
|
|
26
|
+
return core_schema.no_info_after_validator_function(
|
|
27
|
+
cls,
|
|
28
|
+
core_schema.str_schema(),
|
|
29
|
+
serialization=core_schema.to_string_ser_schema(),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def validate_path(self) -> WorkspacePath:
|
|
33
|
+
area, separator, relative = self.partition("/")
|
|
34
|
+
if area not in ("project", "storage", "root") or not separator:
|
|
35
|
+
raise ValueError("Workspace path must start with project/, storage/, or root/.")
|
|
36
|
+
if "\\" in relative or "\0" in relative:
|
|
37
|
+
raise ValueError("Workspace paths use '/' and cannot contain NUL.")
|
|
38
|
+
windows = PureWindowsPath(relative)
|
|
39
|
+
if area == "root":
|
|
40
|
+
native = windows if windows.drive else PurePosixPath(relative)
|
|
41
|
+
if (
|
|
42
|
+
not native.is_absolute()
|
|
43
|
+
or native.as_posix() != relative
|
|
44
|
+
or any(part in (".", "..") for part in relative.split("/"))
|
|
45
|
+
or relative.startswith(("//?/", "//./"))
|
|
46
|
+
):
|
|
47
|
+
raise ValueError("root/ must be followed by a canonical absolute path.")
|
|
48
|
+
return self
|
|
49
|
+
if windows.drive or windows.root or any(
|
|
50
|
+
part in ("", ".", "..") for part in relative.split("/") if relative
|
|
51
|
+
):
|
|
52
|
+
raise ValueError("Workspace path must contain a canonical relative path.")
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def area(self) -> Literal["project", "storage", "root"]:
|
|
57
|
+
return cast(Literal["project", "storage", "root"], self.partition("/")[0])
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def relative(self) -> str:
|
|
61
|
+
# The root/ suffix stays absolute and is rejected by managed-root APIs.
|
|
62
|
+
return self.partition("/")[2] or "."
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def absolute(self) -> Path:
|
|
66
|
+
if self.area != "root":
|
|
67
|
+
raise ValueError("Only root/ paths contain a native absolute path.")
|
|
68
|
+
path = Path(self.relative)
|
|
69
|
+
if not path.is_absolute():
|
|
70
|
+
raise ValueError("The external path is not absolute on this host.")
|
|
71
|
+
return path
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Invocation contexts and links to immutable Workspace provider resources."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ResultResource(BaseModel):
|
|
11
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
12
|
+
|
|
13
|
+
uri: str
|
|
14
|
+
mime_type: Literal["application/json", "text/markdown"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ResultResources(BaseModel):
|
|
18
|
+
resources: dict[str, ResultResource] = Field(default_factory=dict)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class ProviderCall:
|
|
23
|
+
id: str
|
|
24
|
+
tool_name: str
|
|
25
|
+
contexts: Mapping[str, object]
|