polydeukes-sdk 0.19.1__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.
- polydeukes_sdk-0.19.1/.gitignore +6 -0
- polydeukes_sdk-0.19.1/LICENSE +21 -0
- polydeukes_sdk-0.19.1/PKG-INFO +75 -0
- polydeukes_sdk-0.19.1/README.ko.md +57 -0
- polydeukes_sdk-0.19.1/README.md +58 -0
- polydeukes_sdk-0.19.1/pyproject.toml +56 -0
- polydeukes_sdk-0.19.1/src/polydeukes_sdk/__init__.py +49 -0
- polydeukes_sdk-0.19.1/src/polydeukes_sdk/_api.py +131 -0
- polydeukes_sdk-0.19.1/src/polydeukes_sdk/_contract.py +135 -0
- polydeukes_sdk-0.19.1/src/polydeukes_sdk/_process.py +70 -0
- polydeukes_sdk-0.19.1/src/polydeukes_sdk/ir.py +83 -0
- polydeukes_sdk-0.19.1/src/polydeukes_sdk/memory.py +23 -0
- polydeukes_sdk-0.19.1/src/polydeukes_sdk/py.typed +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hyung Soo Seung
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: polydeukes-sdk
|
|
3
|
+
Version: 0.19.1
|
|
4
|
+
Summary: Call the Polydeukes covenant and memory commands from Python
|
|
5
|
+
Project-URL: Repository, https://github.com/huskyhoochu/polydeukes
|
|
6
|
+
Project-URL: Documentation, https://polydeukes.vercel.app
|
|
7
|
+
Author: Polydeukes contributors
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# `polydeukes-sdk` (Python)
|
|
19
|
+
|
|
20
|
+
**English** · [한국어](README.ko.md)
|
|
21
|
+
|
|
22
|
+
Call the Polydeukes judge and memory commands from Python 3.12 or newer. Four synchronous
|
|
23
|
+
verbs share one executable resolver and process runner. The package has zero runtime dependencies.
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
python -m pip install polydeukes-sdk
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Download `pdks-linux-x64` or `pdks-darwin-arm64` from a
|
|
30
|
+
[GitHub Release](https://github.com/huskyhoochu/polydeukes/releases), mark it executable, and
|
|
31
|
+
put it on an absolute `PATH` directory as `pdks`, or pass its path as `executable`.
|
|
32
|
+
Keep the SDK and executable on the same minor release. These are the supported platforms:
|
|
33
|
+
Linux x64 and macOS arm64. On macOS, a browser download may require
|
|
34
|
+
`xattr -d com.apple.quarantine /path/to/pdks`.
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from polydeukes_sdk import (
|
|
38
|
+
CheckCovenantSpec,
|
|
39
|
+
CovenantInput,
|
|
40
|
+
Upheld,
|
|
41
|
+
check_covenant,
|
|
42
|
+
CheckChangeSetSpec,
|
|
43
|
+
check_change_set,
|
|
44
|
+
MemoryIngestSpec,
|
|
45
|
+
memory_ingest,
|
|
46
|
+
MemorySearchSpec,
|
|
47
|
+
memory_search,
|
|
48
|
+
Found,
|
|
49
|
+
Empty,
|
|
50
|
+
Unavailable,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
payload: CovenantInput = {"toolCalls": [], "subagentSpawns": [], "userMessages": []}
|
|
54
|
+
verdict = check_covenant(CheckCovenantSpec(repo_root=".", input=payload, timeout=10))
|
|
55
|
+
if isinstance(verdict, Upheld):
|
|
56
|
+
print("The proposed operation may proceed", verdict.advisories)
|
|
57
|
+
|
|
58
|
+
verdict = check_change_set(CheckChangeSetSpec(repo_root=".", diff="", timeout=10))
|
|
59
|
+
ingest = memory_ingest(MemoryIngestSpec(root=".", timeout=30))
|
|
60
|
+
recall = memory_search(MemorySearchSpec(root=".", query="design decision", limit=5, timeout=10))
|
|
61
|
+
match recall:
|
|
62
|
+
case Found(hits=hits):
|
|
63
|
+
print(hits[0]["sectionTitle"])
|
|
64
|
+
case Empty():
|
|
65
|
+
print("No matches")
|
|
66
|
+
case Unavailable(reason=reason):
|
|
67
|
+
print("Memory unavailable:", reason)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The host supplies IR evidence and decides whether to execute the proposed operation. Only
|
|
71
|
+
`Upheld` permits it in this example. The child reads configuration, judges disciplines, writes
|
|
72
|
+
telemetry, and manages the memory index.
|
|
73
|
+
|
|
74
|
+
The [Python SDK reference](https://github.com/huskyhoochu/polydeukes/blob/main/docs/reference/packages/sdk-python.md)
|
|
75
|
+
describes every spec, outcome, timeout, and the injectable `spawn` boundary.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# `polydeukes-sdk` (Python)
|
|
2
|
+
|
|
3
|
+
[English](README.md) · **한국어**
|
|
4
|
+
|
|
5
|
+
Python 3.12 이상에서 Polydeukes 판정기와 기억(memory) 명령을 호출합니다. 동기 동사 네 개가
|
|
6
|
+
실행 파일 해석기와 프로세스 실행기 하나를 공유합니다. 런타임 의존성은 없습니다.
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
python -m pip install polydeukes-sdk
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
[GitHub Release](https://github.com/huskyhoochu/polydeukes/releases)에서 `pdks-linux-x64` 또는
|
|
13
|
+
`pdks-darwin-arm64`를 받아 실행 권한을 부여하고, 절대 `PATH` 디렉터리에 `pdks`라는 이름으로
|
|
14
|
+
두거나 `executable`에 경로를 지정합니다. SDK와 실행 파일의 minor 버전을 맞춥니다. 지원 플랫폼은
|
|
15
|
+
Linux x64와 macOS arm64입니다. macOS에서 브라우저로 받았다면
|
|
16
|
+
`xattr -d com.apple.quarantine /path/to/pdks`가 필요할 수 있습니다.
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from polydeukes_sdk import (
|
|
20
|
+
CheckCovenantSpec,
|
|
21
|
+
CovenantInput,
|
|
22
|
+
Upheld,
|
|
23
|
+
check_covenant,
|
|
24
|
+
CheckChangeSetSpec,
|
|
25
|
+
check_change_set,
|
|
26
|
+
MemoryIngestSpec,
|
|
27
|
+
memory_ingest,
|
|
28
|
+
MemorySearchSpec,
|
|
29
|
+
memory_search,
|
|
30
|
+
Found,
|
|
31
|
+
Empty,
|
|
32
|
+
Unavailable,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
payload: CovenantInput = {"toolCalls": [], "subagentSpawns": [], "userMessages": []}
|
|
36
|
+
verdict = check_covenant(CheckCovenantSpec(repo_root=".", input=payload, timeout=10))
|
|
37
|
+
if isinstance(verdict, Upheld):
|
|
38
|
+
print("제안한 작업을 진행할 수 있습니다", verdict.advisories)
|
|
39
|
+
|
|
40
|
+
verdict = check_change_set(CheckChangeSetSpec(repo_root=".", diff="", timeout=10))
|
|
41
|
+
ingest = memory_ingest(MemoryIngestSpec(root=".", timeout=30))
|
|
42
|
+
recall = memory_search(MemorySearchSpec(root=".", query="설계 결정", limit=5, timeout=10))
|
|
43
|
+
match recall:
|
|
44
|
+
case Found(hits=hits):
|
|
45
|
+
print(hits[0]["sectionTitle"])
|
|
46
|
+
case Empty():
|
|
47
|
+
print("검색 결과 없음")
|
|
48
|
+
case Unavailable(reason=reason):
|
|
49
|
+
print("기억을 사용할 수 없음:", reason)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
호스트는 IR 증거를 제공하고 제안한 작업의 실행 여부를 결정합니다. 이 예시는 `Upheld`일 때만
|
|
53
|
+
실행을 허용합니다. 자식 프로세스가 설정을 읽고 규율(discipline)을 판정하며 텔레메트리와 기억
|
|
54
|
+
색인을 관리합니다.
|
|
55
|
+
|
|
56
|
+
[Python SDK 참조](https://github.com/huskyhoochu/polydeukes/blob/main/docs/reference/packages/sdk-python.ko.md)에서
|
|
57
|
+
모든 spec, 결과, 기한과 주입 가능한 `spawn` 경계를 설명합니다.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# `polydeukes-sdk` (Python)
|
|
2
|
+
|
|
3
|
+
**English** · [한국어](README.ko.md)
|
|
4
|
+
|
|
5
|
+
Call the Polydeukes judge and memory commands from Python 3.12 or newer. Four synchronous
|
|
6
|
+
verbs share one executable resolver and process runner. The package has zero runtime dependencies.
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
python -m pip install polydeukes-sdk
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Download `pdks-linux-x64` or `pdks-darwin-arm64` from a
|
|
13
|
+
[GitHub Release](https://github.com/huskyhoochu/polydeukes/releases), mark it executable, and
|
|
14
|
+
put it on an absolute `PATH` directory as `pdks`, or pass its path as `executable`.
|
|
15
|
+
Keep the SDK and executable on the same minor release. These are the supported platforms:
|
|
16
|
+
Linux x64 and macOS arm64. On macOS, a browser download may require
|
|
17
|
+
`xattr -d com.apple.quarantine /path/to/pdks`.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from polydeukes_sdk import (
|
|
21
|
+
CheckCovenantSpec,
|
|
22
|
+
CovenantInput,
|
|
23
|
+
Upheld,
|
|
24
|
+
check_covenant,
|
|
25
|
+
CheckChangeSetSpec,
|
|
26
|
+
check_change_set,
|
|
27
|
+
MemoryIngestSpec,
|
|
28
|
+
memory_ingest,
|
|
29
|
+
MemorySearchSpec,
|
|
30
|
+
memory_search,
|
|
31
|
+
Found,
|
|
32
|
+
Empty,
|
|
33
|
+
Unavailable,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
payload: CovenantInput = {"toolCalls": [], "subagentSpawns": [], "userMessages": []}
|
|
37
|
+
verdict = check_covenant(CheckCovenantSpec(repo_root=".", input=payload, timeout=10))
|
|
38
|
+
if isinstance(verdict, Upheld):
|
|
39
|
+
print("The proposed operation may proceed", verdict.advisories)
|
|
40
|
+
|
|
41
|
+
verdict = check_change_set(CheckChangeSetSpec(repo_root=".", diff="", timeout=10))
|
|
42
|
+
ingest = memory_ingest(MemoryIngestSpec(root=".", timeout=30))
|
|
43
|
+
recall = memory_search(MemorySearchSpec(root=".", query="design decision", limit=5, timeout=10))
|
|
44
|
+
match recall:
|
|
45
|
+
case Found(hits=hits):
|
|
46
|
+
print(hits[0]["sectionTitle"])
|
|
47
|
+
case Empty():
|
|
48
|
+
print("No matches")
|
|
49
|
+
case Unavailable(reason=reason):
|
|
50
|
+
print("Memory unavailable:", reason)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The host supplies IR evidence and decides whether to execute the proposed operation. Only
|
|
54
|
+
`Upheld` permits it in this example. The child reads configuration, judges disciplines, writes
|
|
55
|
+
telemetry, and manages the memory index.
|
|
56
|
+
|
|
57
|
+
The [Python SDK reference](https://github.com/huskyhoochu/polydeukes/blob/main/docs/reference/packages/sdk-python.md)
|
|
58
|
+
describes every spec, outcome, timeout, and the injectable `spawn` boundary.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling==1.32.4"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "polydeukes-sdk"
|
|
7
|
+
version = "0.19.1"
|
|
8
|
+
description = "Call the Polydeukes covenant and memory commands from Python"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{name = "Polydeukes contributors"}]
|
|
14
|
+
dependencies = []
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.14",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Repository = "https://github.com/huskyhoochu/polydeukes"
|
|
25
|
+
Documentation = "https://polydeukes.vercel.app"
|
|
26
|
+
|
|
27
|
+
[dependency-groups]
|
|
28
|
+
dev = [
|
|
29
|
+
"datamodel-code-generator==0.86.0",
|
|
30
|
+
"pyright==1.1.414",
|
|
31
|
+
"ruff==0.17.1",
|
|
32
|
+
"build==1.6.1",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/polydeukes_sdk"]
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.sdist]
|
|
39
|
+
include = ["src/", "README.md", "README.ko.md", "LICENSE", "pyproject.toml"]
|
|
40
|
+
|
|
41
|
+
[tool.pyright]
|
|
42
|
+
include = ["src", "tests/typing/valid.py"]
|
|
43
|
+
extraPaths = ["src"]
|
|
44
|
+
pythonVersion = "3.12"
|
|
45
|
+
typeCheckingMode = "strict"
|
|
46
|
+
|
|
47
|
+
[tool.ruff]
|
|
48
|
+
target-version = "py312"
|
|
49
|
+
line-length = 100
|
|
50
|
+
exclude = ["src/polydeukes_sdk/ir.py", "src/polydeukes_sdk/memory.py"]
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint]
|
|
53
|
+
select = ["E4", "E7", "E9", "F", "I"]
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint.isort]
|
|
56
|
+
combine-as-imports = true
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Typed synchronous clients for a standalone pdks executable."""
|
|
2
|
+
|
|
3
|
+
from ._api import (
|
|
4
|
+
check_change_set as check_change_set,
|
|
5
|
+
check_covenant as check_covenant,
|
|
6
|
+
memory_ingest as memory_ingest,
|
|
7
|
+
memory_search as memory_search,
|
|
8
|
+
)
|
|
9
|
+
from ._contract import (
|
|
10
|
+
Blocked as Blocked,
|
|
11
|
+
CheckChangeSetSpec as CheckChangeSetSpec,
|
|
12
|
+
CheckCovenantSpec as CheckCovenantSpec,
|
|
13
|
+
CheckCovenantVerdict as CheckCovenantVerdict,
|
|
14
|
+
Empty as Empty,
|
|
15
|
+
Enforce as Enforce,
|
|
16
|
+
Found as Found,
|
|
17
|
+
Ingested as Ingested,
|
|
18
|
+
MemoryIngestOutcome as MemoryIngestOutcome,
|
|
19
|
+
MemoryIngestSpec as MemoryIngestSpec,
|
|
20
|
+
MemorySearchOutcome as MemorySearchOutcome,
|
|
21
|
+
MemorySearchSpec as MemorySearchSpec,
|
|
22
|
+
Path as Path,
|
|
23
|
+
Spawn as Spawn,
|
|
24
|
+
SpawnOutcome as SpawnOutcome,
|
|
25
|
+
SpawnSpec as SpawnSpec,
|
|
26
|
+
Unavailable as Unavailable,
|
|
27
|
+
Unjudged as Unjudged,
|
|
28
|
+
Upheld as Upheld,
|
|
29
|
+
)
|
|
30
|
+
from .ir import (
|
|
31
|
+
Actor as Actor,
|
|
32
|
+
Channels as Channels,
|
|
33
|
+
CovenantInput as CovenantInput,
|
|
34
|
+
FileChange as FileChange,
|
|
35
|
+
FileChangeCreate as FileChangeCreate,
|
|
36
|
+
FileChangeDelete as FileChangeDelete,
|
|
37
|
+
FileChangeModify as FileChangeModify,
|
|
38
|
+
Session as Session,
|
|
39
|
+
SessionToolCall as SessionToolCall,
|
|
40
|
+
SessionUserMessage as SessionUserMessage,
|
|
41
|
+
SubagentSpawn as SubagentSpawn,
|
|
42
|
+
ToolCall as ToolCall,
|
|
43
|
+
Tools as Tools,
|
|
44
|
+
UserMessage as UserMessage,
|
|
45
|
+
)
|
|
46
|
+
from .memory import (
|
|
47
|
+
MemorySearchOutput as MemorySearchOutput,
|
|
48
|
+
MemorySearchResult as MemorySearchResult,
|
|
49
|
+
)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Four synchronous process clients for the pdks executable."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
from ._contract import (
|
|
8
|
+
Blocked,
|
|
9
|
+
CheckChangeSetSpec,
|
|
10
|
+
CheckCovenantSpec,
|
|
11
|
+
CheckCovenantVerdict,
|
|
12
|
+
Empty,
|
|
13
|
+
Found,
|
|
14
|
+
Ingested,
|
|
15
|
+
MemoryIngestOutcome,
|
|
16
|
+
MemoryIngestSpec,
|
|
17
|
+
MemorySearchOutcome,
|
|
18
|
+
MemorySearchSpec,
|
|
19
|
+
SpawnOutcome,
|
|
20
|
+
Unavailable,
|
|
21
|
+
Unjudged,
|
|
22
|
+
Upheld,
|
|
23
|
+
)
|
|
24
|
+
from ._process import run
|
|
25
|
+
from .memory import MemorySearchResult
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _reason(error: Exception) -> str:
|
|
29
|
+
return str(error) or type(error).__name__
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _failure(outcome: SpawnOutcome) -> str:
|
|
33
|
+
return outcome.stderr.decode("utf-8", errors="replace") or (
|
|
34
|
+
f"pdks exited with status {outcome.status}"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _check(spec: CheckCovenantSpec | CheckChangeSetSpec) -> CheckCovenantVerdict:
|
|
39
|
+
try:
|
|
40
|
+
args = ("covenant", "check")
|
|
41
|
+
if isinstance(spec, CheckChangeSetSpec):
|
|
42
|
+
args += ("--diff",)
|
|
43
|
+
stdin = spec.diff.encode("utf-8")
|
|
44
|
+
else:
|
|
45
|
+
stdin = json.dumps(spec.input, ensure_ascii=False, allow_nan=False).encode("utf-8")
|
|
46
|
+
args += ("--enforce", spec.enforce)
|
|
47
|
+
if spec.config_layer is not None:
|
|
48
|
+
args += ("--config-layer", os.fspath(spec.config_layer))
|
|
49
|
+
if spec.telemetry_path is not None:
|
|
50
|
+
args += ("--telemetry-path", os.fspath(spec.telemetry_path))
|
|
51
|
+
outcome = run(spec.repo_root, spec.executable, spec.timeout, spec.spawn, args, stdin)
|
|
52
|
+
if outcome.status == 0:
|
|
53
|
+
return Upheld(outcome.stderr.decode("utf-8"))
|
|
54
|
+
if outcome.status == 2:
|
|
55
|
+
return Blocked(outcome.stderr.decode("utf-8"))
|
|
56
|
+
return Unjudged(_failure(outcome))
|
|
57
|
+
except Exception as error:
|
|
58
|
+
return Unjudged(_reason(error))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def check_covenant(spec: CheckCovenantSpec) -> CheckCovenantVerdict:
|
|
62
|
+
"""Check caller-provided IR; only Upheld authorizes the proposed action."""
|
|
63
|
+
return _check(spec)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def check_change_set(spec: CheckChangeSetSpec) -> CheckCovenantVerdict:
|
|
67
|
+
"""Check a unified diff without changing its content or line endings."""
|
|
68
|
+
return _check(spec)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def memory_ingest(spec: MemoryIngestSpec) -> MemoryIngestOutcome:
|
|
72
|
+
"""Ingest through pdks, returning Unavailable for every process failure."""
|
|
73
|
+
try:
|
|
74
|
+
outcome = run(
|
|
75
|
+
spec.root, spec.executable, spec.timeout, spec.spawn, ("memory", "ingest"), b""
|
|
76
|
+
)
|
|
77
|
+
return Ingested() if outcome.status == 0 else Unavailable(_failure(outcome))
|
|
78
|
+
except Exception as error:
|
|
79
|
+
return Unavailable(_reason(error))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _invalid_constant(value: str) -> object:
|
|
83
|
+
raise ValueError(f"Invalid JSON constant: {value}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _hits(data: bytes) -> tuple[MemorySearchResult, ...]:
|
|
87
|
+
payload: object = json.loads(data.decode("utf-8"), parse_constant=_invalid_constant)
|
|
88
|
+
if not isinstance(payload, dict):
|
|
89
|
+
raise ValueError("memory search response must be an object")
|
|
90
|
+
root = cast(dict[str, object], payload)
|
|
91
|
+
results = root.get("results")
|
|
92
|
+
if not isinstance(root.get("ingestedAt"), str) or not isinstance(results, list):
|
|
93
|
+
raise ValueError("memory search response requires ingestedAt and results")
|
|
94
|
+
for value in cast(list[object], results):
|
|
95
|
+
if not isinstance(value, dict):
|
|
96
|
+
raise ValueError("memory search hit must be an object")
|
|
97
|
+
hit = cast(dict[str, object], value)
|
|
98
|
+
if not all(
|
|
99
|
+
isinstance(hit.get(field), str)
|
|
100
|
+
for field in ("id", "conceptId", "docTitle", "sectionTitle", "status")
|
|
101
|
+
):
|
|
102
|
+
raise ValueError("memory search hit requires string identity and status fields")
|
|
103
|
+
if hit.get("trust") not in ("human-reviewed", "machine-verified", "unverified"):
|
|
104
|
+
raise ValueError("memory search hit has invalid trust")
|
|
105
|
+
if hit.get("matchPath") not in ("and", "or", "like"):
|
|
106
|
+
raise ValueError("memory search hit has invalid matchPath")
|
|
107
|
+
superseded = hit.get("supersededBy")
|
|
108
|
+
if (
|
|
109
|
+
not isinstance(hit.get("stale"), bool)
|
|
110
|
+
or not isinstance(superseded, list)
|
|
111
|
+
or not all(isinstance(item, str) for item in cast(list[object], superseded))
|
|
112
|
+
):
|
|
113
|
+
raise ValueError("memory search hit requires stale and supersededBy")
|
|
114
|
+
return tuple(cast(list[MemorySearchResult], results))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def memory_search(spec: MemorySearchSpec) -> MemorySearchOutcome:
|
|
118
|
+
"""Return ranked hits, a valid empty search, or an unavailable response."""
|
|
119
|
+
try:
|
|
120
|
+
args = ("memory", "search", spec.query, "--json")
|
|
121
|
+
if spec.limit is not None:
|
|
122
|
+
if type(spec.limit) is not int or spec.limit <= 0:
|
|
123
|
+
raise ValueError("limit must be a positive integer")
|
|
124
|
+
args += ("--limit", str(spec.limit))
|
|
125
|
+
outcome = run(spec.root, spec.executable, spec.timeout, spec.spawn, args, b"")
|
|
126
|
+
if outcome.status != 0:
|
|
127
|
+
return Unavailable(_failure(outcome))
|
|
128
|
+
hits = _hits(outcome.stdout)
|
|
129
|
+
return Found(hits) if hits else Empty()
|
|
130
|
+
except Exception as error:
|
|
131
|
+
return Unavailable(_reason(error))
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Immutable call specifications and result variants."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from os import PathLike
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from .ir import CovenantInput
|
|
9
|
+
from .memory import MemorySearchResult
|
|
10
|
+
|
|
11
|
+
type Path = str | PathLike[str]
|
|
12
|
+
type Enforce = Literal["advise", "block"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, kw_only=True)
|
|
16
|
+
class SpawnSpec:
|
|
17
|
+
"""One direct executable invocation; an injected spawn enforces its timeout."""
|
|
18
|
+
|
|
19
|
+
command: str
|
|
20
|
+
args: tuple[str, ...]
|
|
21
|
+
cwd: str
|
|
22
|
+
stdin: bytes
|
|
23
|
+
timeout: float | None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, kw_only=True)
|
|
27
|
+
class SpawnOutcome:
|
|
28
|
+
"""Captured process output, with a missing status representing a signal."""
|
|
29
|
+
|
|
30
|
+
status: int | None
|
|
31
|
+
stdout: bytes
|
|
32
|
+
stderr: bytes
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
type Spawn = Callable[[SpawnSpec], SpawnOutcome]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, kw_only=True)
|
|
39
|
+
class CheckCovenantSpec:
|
|
40
|
+
"""Check a caller-built input in the repository root."""
|
|
41
|
+
|
|
42
|
+
repo_root: Path
|
|
43
|
+
input: CovenantInput
|
|
44
|
+
enforce: Enforce = "block"
|
|
45
|
+
config_layer: Path | None = None
|
|
46
|
+
telemetry_path: Path | None = None
|
|
47
|
+
executable: Path | None = None
|
|
48
|
+
timeout: float | None = None
|
|
49
|
+
spawn: Spawn | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True, kw_only=True)
|
|
53
|
+
class CheckChangeSetSpec:
|
|
54
|
+
"""Check the exact UTF-8 bytes of a caller's unified diff."""
|
|
55
|
+
|
|
56
|
+
repo_root: Path
|
|
57
|
+
diff: str
|
|
58
|
+
enforce: Enforce = "block"
|
|
59
|
+
config_layer: Path | None = None
|
|
60
|
+
telemetry_path: Path | None = None
|
|
61
|
+
executable: Path | None = None
|
|
62
|
+
timeout: float | None = None
|
|
63
|
+
spawn: Spawn | None = None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True, kw_only=True)
|
|
67
|
+
class MemoryIngestSpec:
|
|
68
|
+
"""Ask the executable to ingest documents under root."""
|
|
69
|
+
|
|
70
|
+
root: Path
|
|
71
|
+
executable: Path | None = None
|
|
72
|
+
timeout: float | None = None
|
|
73
|
+
spawn: Spawn | None = None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True, kw_only=True)
|
|
77
|
+
class MemorySearchSpec:
|
|
78
|
+
"""Search the executable's memory index with one literal query argument."""
|
|
79
|
+
|
|
80
|
+
root: Path
|
|
81
|
+
query: str
|
|
82
|
+
limit: int | None = None
|
|
83
|
+
executable: Path | None = None
|
|
84
|
+
timeout: float | None = None
|
|
85
|
+
spawn: Spawn | None = None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class Upheld:
|
|
90
|
+
"""The executable upheld the covenant, possibly with advisories."""
|
|
91
|
+
|
|
92
|
+
advisories: str
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True)
|
|
96
|
+
class Blocked:
|
|
97
|
+
"""The covenant command returned its blocking exit status."""
|
|
98
|
+
|
|
99
|
+
reason: str
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True)
|
|
103
|
+
class Unjudged:
|
|
104
|
+
"""No covenant verdict was available."""
|
|
105
|
+
|
|
106
|
+
reason: str
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True)
|
|
110
|
+
class Ingested:
|
|
111
|
+
"""The memory ingest command succeeded."""
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True)
|
|
115
|
+
class Unavailable:
|
|
116
|
+
"""The memory command or its response could not be consumed."""
|
|
117
|
+
|
|
118
|
+
reason: str
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass(frozen=True)
|
|
122
|
+
class Found:
|
|
123
|
+
"""Nonempty search hits in the executable's ranking order."""
|
|
124
|
+
|
|
125
|
+
hits: tuple[MemorySearchResult, ...]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass(frozen=True)
|
|
129
|
+
class Empty:
|
|
130
|
+
"""A valid successful search returned no hits."""
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
type CheckCovenantVerdict = Upheld | Blocked | Unjudged
|
|
134
|
+
type MemoryIngestOutcome = Ingested | Unavailable
|
|
135
|
+
type MemorySearchOutcome = Found | Empty | Unavailable
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Executable resolution and binary process transport shared by all verbs."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
from ._contract import Path, Spawn, SpawnOutcome, SpawnSpec
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _absolute(path: Path, cwd: str) -> str:
|
|
11
|
+
return os.path.normpath(os.path.join(cwd, os.fspath(path)))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _resolve(executable: Path | None, cwd: str) -> str:
|
|
15
|
+
if executable is not None:
|
|
16
|
+
return _absolute(executable, cwd)
|
|
17
|
+
for directory in os.environ.get("PATH", "").split(os.pathsep):
|
|
18
|
+
if os.path.isabs(directory):
|
|
19
|
+
candidate = os.path.join(directory, "pdks")
|
|
20
|
+
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
|
|
21
|
+
return candidate
|
|
22
|
+
raise FileNotFoundError("No executable pdks found in absolute PATH directories")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _spawn(spec: SpawnSpec) -> SpawnOutcome:
|
|
26
|
+
child = subprocess.Popen(
|
|
27
|
+
(spec.command, *spec.args),
|
|
28
|
+
cwd=spec.cwd,
|
|
29
|
+
stdin=subprocess.PIPE,
|
|
30
|
+
stdout=subprocess.PIPE,
|
|
31
|
+
stderr=subprocess.PIPE,
|
|
32
|
+
close_fds=True,
|
|
33
|
+
)
|
|
34
|
+
try:
|
|
35
|
+
stdout, stderr = child.communicate(spec.stdin, timeout=spec.timeout)
|
|
36
|
+
except BaseException:
|
|
37
|
+
child.kill()
|
|
38
|
+
child.wait()
|
|
39
|
+
# A surviving descendant can keep these pipes open after the direct child exits.
|
|
40
|
+
for stream in (child.stdin, child.stdout, child.stderr):
|
|
41
|
+
if stream is not None:
|
|
42
|
+
stream.close()
|
|
43
|
+
raise
|
|
44
|
+
return SpawnOutcome(
|
|
45
|
+
status=child.returncode if child.returncode >= 0 else None,
|
|
46
|
+
stdout=stdout,
|
|
47
|
+
stderr=stderr,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def run(
|
|
52
|
+
root: Path,
|
|
53
|
+
executable: Path | None,
|
|
54
|
+
timeout: float | None,
|
|
55
|
+
spawn: Spawn | None,
|
|
56
|
+
args: tuple[str, ...],
|
|
57
|
+
stdin: bytes,
|
|
58
|
+
) -> SpawnOutcome:
|
|
59
|
+
if timeout is not None and (not math.isfinite(timeout) or timeout < 0):
|
|
60
|
+
raise ValueError("timeout must be finite and nonnegative")
|
|
61
|
+
cwd = os.getcwd()
|
|
62
|
+
return (spawn or _spawn)(
|
|
63
|
+
SpawnSpec(
|
|
64
|
+
command=_resolve(executable, cwd),
|
|
65
|
+
args=args,
|
|
66
|
+
cwd=_absolute(root, cwd),
|
|
67
|
+
stdin=stdin,
|
|
68
|
+
timeout=timeout,
|
|
69
|
+
)
|
|
70
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# generated by datamodel-codegen:
|
|
2
|
+
# filename: covenant-input.schema.json
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any, Literal, NotRequired, TypedDict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FileChangeCreate(TypedDict):
|
|
10
|
+
kind: Literal['create']
|
|
11
|
+
path: str
|
|
12
|
+
post: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FileChangeModify(TypedDict):
|
|
16
|
+
kind: Literal['modify']
|
|
17
|
+
path: str
|
|
18
|
+
pre: str
|
|
19
|
+
post: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FileChangeDelete(TypedDict):
|
|
23
|
+
kind: Literal['delete']
|
|
24
|
+
path: str
|
|
25
|
+
pre: NotRequired[str]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
type FileChange = FileChangeCreate | FileChangeModify | FileChangeDelete
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SubagentSpawn(TypedDict):
|
|
32
|
+
kind: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class UserMessage(TypedDict):
|
|
36
|
+
text: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Channels(TypedDict):
|
|
40
|
+
sidecar: NotRequired[str]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Actor(TypedDict):
|
|
44
|
+
agentType: NotRequired[str]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Tools(TypedDict):
|
|
48
|
+
mutating: list[str]
|
|
49
|
+
shell: list[str]
|
|
50
|
+
commandArgs: list[str]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SessionUserMessage(TypedDict):
|
|
54
|
+
text: str
|
|
55
|
+
timestampMs: NotRequired[float]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SessionToolCall(TypedDict):
|
|
59
|
+
name: str
|
|
60
|
+
args: NotRequired[dict[str, Any]]
|
|
61
|
+
succeeded: NotRequired[bool]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ToolCall(TypedDict):
|
|
65
|
+
name: str
|
|
66
|
+
args: NotRequired[dict[str, Any]]
|
|
67
|
+
fileChange: NotRequired[FileChange]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Session(TypedDict):
|
|
71
|
+
evidencePath: NotRequired[str]
|
|
72
|
+
userMessages: list[SessionUserMessage]
|
|
73
|
+
toolCalls: list[SessionToolCall]
|
|
74
|
+
channels: NotRequired[Channels]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class CovenantInput(TypedDict):
|
|
78
|
+
toolCalls: list[ToolCall]
|
|
79
|
+
subagentSpawns: list[SubagentSpawn]
|
|
80
|
+
userMessages: list[UserMessage]
|
|
81
|
+
actor: NotRequired[Actor]
|
|
82
|
+
tools: NotRequired[Tools]
|
|
83
|
+
session: NotRequired[Session]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# generated by datamodel-codegen:
|
|
2
|
+
# filename: memory-search-output.schema.json
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Literal, TypedDict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MemorySearchResult(TypedDict):
|
|
10
|
+
id: str
|
|
11
|
+
conceptId: str
|
|
12
|
+
docTitle: str
|
|
13
|
+
sectionTitle: str
|
|
14
|
+
status: str
|
|
15
|
+
trust: Literal['human-reviewed', 'machine-verified', 'unverified']
|
|
16
|
+
stale: bool
|
|
17
|
+
matchPath: Literal['and', 'or', 'like']
|
|
18
|
+
supersededBy: list[str]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MemorySearchOutput(TypedDict):
|
|
22
|
+
ingestedAt: str
|
|
23
|
+
results: list[MemorySearchResult]
|
|
File without changes
|