kimi-agent-module-api 2.1.0__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. {kimi_agent_module_api-2.1.0/src/kimi_agent_module_api.egg-info → kimi_agent_module_api-2.2.0}/PKG-INFO +15 -1
  2. {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.2.0}/README.md +14 -0
  3. {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.2.0}/pyproject.toml +1 -1
  4. {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.2.0}/src/kimi_agent_module_api/__init__.py +5 -0
  5. {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.2.0}/src/kimi_agent_module_api/contracts.py +2 -0
  6. kimi_agent_module_api-2.2.0/src/kimi_agent_module_api/files.py +61 -0
  7. {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.2.0}/src/kimi_agent_module_api/testing.py +66 -0
  8. {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.2.0}/src/kimi_agent_module_api/tools.py +4 -0
  9. {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.2.0/src/kimi_agent_module_api.egg-info}/PKG-INFO +15 -1
  10. {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.2.0}/src/kimi_agent_module_api.egg-info/SOURCES.txt +2 -0
  11. {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.2.0}/tests/test_contracts.py +2 -2
  12. kimi_agent_module_api-2.2.0/tests/test_files.py +41 -0
  13. {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.2.0}/LICENSE +0 -0
  14. {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.2.0}/setup.cfg +0 -0
  15. {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.2.0}/src/kimi_agent_module_api/events.py +0 -0
  16. {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.2.0}/src/kimi_agent_module_api/images.py +0 -0
  17. {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.2.0}/src/kimi_agent_module_api/py.typed +0 -0
  18. {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.2.0}/src/kimi_agent_module_api/settings.py +0 -0
  19. {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.2.0}/src/kimi_agent_module_api/trust.py +0 -0
  20. {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.2.0}/src/kimi_agent_module_api.egg-info/dependency_links.txt +0 -0
  21. {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.2.0}/src/kimi_agent_module_api.egg-info/requires.txt +0 -0
  22. {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.2.0}/src/kimi_agent_module_api.egg-info/top_level.txt +0 -0
  23. {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.2.0}/tests/test_public_api.py +0 -0
  24. {kimi_agent_module_api-2.1.0 → kimi_agent_module_api-2.2.0}/tests/test_testing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kimi-agent-module-api
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: Stable contracts for community-built assistant modules
5
5
  Author: Webhead
6
6
  License-Expression: MIT
@@ -30,6 +30,8 @@ implementation, or module loader. It exports:
30
30
  (storage, scheduler, events, Discord actions, interactions, HTTP, services,
31
31
  trust, proposals, health) plus the validators the host runs at preflight.
32
32
  - `kimi_agent_module_api.events`: the normalized `discord.*` event payloads.
33
+ - `kimi_agent_module_api.files`: `ToolFiles`, `ToolAttachment`, `ToolFile`, and
34
+ `FileAccessError` for invocation-scoped attachment and workspace reads.
33
35
  - `kimi_agent_module_api.testing`: a fake for every port, `load_context()` for
34
36
  exercising `create()`, and `MemoryStorage` (install the `testing` extra) so a
35
37
  module can unit test itself with only this package installed.
@@ -91,6 +93,18 @@ calls receive the exact source Discord message snowflake; other surfaces receive
91
93
  Modules use namespaced guild documents and the physical table names returned
92
94
  by `ctx.storage.table()`.
93
95
 
96
+ SDK 2.2 adds optional `ModuleToolContext.files`. Declare
97
+ `ModulePermissions(tool_files=True)`, depend on `kimi-agent-module-api>=2.2,<3`,
98
+ and require `tools.files.v1` in `ModuleSpec.requires_capabilities`. Existing modules
99
+ keep `api_version=2` and receive `files=None` without the permission.
100
+ `ctx.files.attachments` lists admitted current/reply entries;
101
+ `read_attachment(id, max_bytes=...)` and `read_workspace(path, max_bytes=...)`
102
+ return bounded bytes without network downloads. The reader expires when the handler
103
+ returns and is scoped to the actual caller. `testing.FakeToolFiles` supports
104
+ independent tests. See the
105
+ [file access guide](https://github.com/webhead2oo9/kimi-agent/blob/main/docs/module-files.md)
106
+ for moderation, reply-image availability, privacy, and limits.
107
+
94
108
  ## Testing the SDK
95
109
 
96
110
  From this package directory, run its tests without installing the Kimi application:
@@ -13,6 +13,8 @@ implementation, or module loader. It exports:
13
13
  (storage, scheduler, events, Discord actions, interactions, HTTP, services,
14
14
  trust, proposals, health) plus the validators the host runs at preflight.
15
15
  - `kimi_agent_module_api.events`: the normalized `discord.*` event payloads.
16
+ - `kimi_agent_module_api.files`: `ToolFiles`, `ToolAttachment`, `ToolFile`, and
17
+ `FileAccessError` for invocation-scoped attachment and workspace reads.
16
18
  - `kimi_agent_module_api.testing`: a fake for every port, `load_context()` for
17
19
  exercising `create()`, and `MemoryStorage` (install the `testing` extra) so a
18
20
  module can unit test itself with only this package installed.
@@ -74,6 +76,18 @@ calls receive the exact source Discord message snowflake; other surfaces receive
74
76
  Modules use namespaced guild documents and the physical table names returned
75
77
  by `ctx.storage.table()`.
76
78
 
79
+ SDK 2.2 adds optional `ModuleToolContext.files`. Declare
80
+ `ModulePermissions(tool_files=True)`, depend on `kimi-agent-module-api>=2.2,<3`,
81
+ and require `tools.files.v1` in `ModuleSpec.requires_capabilities`. Existing modules
82
+ keep `api_version=2` and receive `files=None` without the permission.
83
+ `ctx.files.attachments` lists admitted current/reply entries;
84
+ `read_attachment(id, max_bytes=...)` and `read_workspace(path, max_bytes=...)`
85
+ return bounded bytes without network downloads. The reader expires when the handler
86
+ returns and is scoped to the actual caller. `testing.FakeToolFiles` supports
87
+ independent tests. See the
88
+ [file access guide](https://github.com/webhead2oo9/kimi-agent/blob/main/docs/module-files.md)
89
+ for moderation, reply-image availability, privacy, and limits.
90
+
77
91
  ## Testing the SDK
78
92
 
79
93
  From this package directory, run its tests without installing the Kimi application:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kimi-agent-module-api"
3
- version = "2.1.0"
3
+ version = "2.2.0"
4
4
  description = "Stable contracts for community-built assistant modules"
5
5
  requires-python = ">=3.14"
6
6
  authors = [{ name = "Webhead" }]
@@ -37,6 +37,7 @@ from kimi_agent_module_api.contracts import (
37
37
  TextInputStyle,
38
38
  )
39
39
  from kimi_agent_module_api.settings import ModuleSetting, ModuleSettingsDefinition
40
+ from kimi_agent_module_api.files import FileAccessError, ToolAttachment, ToolFile, ToolFiles
40
41
  from kimi_agent_module_api.tools import (
41
42
  ModuleToolContext,
42
43
  ModuleToolHandler,
@@ -152,6 +153,7 @@ __all__ = [
152
153
  "MODULE_ENTRYPOINT_GROUP",
153
154
  "AppModule",
154
155
  "ConfigSnapshot",
156
+ "FileAccessError",
155
157
  "GuildSettingsSchema",
156
158
  "InviteSnapshot",
157
159
  "LayoutGallery",
@@ -183,6 +185,9 @@ __all__ = [
183
185
  "ServiceRequirement",
184
186
  "TextInputSpec",
185
187
  "TextInputStyle",
188
+ "ToolAttachment",
189
+ "ToolFile",
190
+ "ToolFiles",
186
191
  "TrustTier",
187
192
  "render_guild_settings",
188
193
  ]
@@ -143,6 +143,8 @@ class ModulePermissions:
143
143
  override_target_policy: bool = False
144
144
  raw_bot: bool = False
145
145
  raw_storage: bool = False
146
+ # Read admitted attachments and caller-owned workspace files in tool handlers.
147
+ tool_files: bool = False
146
148
 
147
149
 
148
150
  @dataclass(frozen=True, slots=True)
@@ -0,0 +1,61 @@
1
+ """Read-only, caller-scoped file access during one module tool invocation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Literal, Protocol
7
+
8
+
9
+ class FileAccessError(ValueError):
10
+ """A bounded, safe-to-display file error; never includes host paths or URLs."""
11
+
12
+ def __init__(self, code: str, message: str) -> None:
13
+ super().__init__(message)
14
+ self.code = code
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class ToolAttachment:
19
+ """An admitted attachment or an explicit unavailable entry.
20
+
21
+ ``id`` is opaque and valid only for this invocation. ``workspace_path`` is
22
+ relative to the caller's workspace, and may be reused in later turns.
23
+ Reply images have generated filenames when the host has no source name.
24
+ No Discord SDK objects, signed URLs, or absolute paths are exposed.
25
+ """
26
+
27
+ id: str
28
+ filename: str
29
+ size: int
30
+ media_type: str | None
31
+ source: Literal["current", "reply"] = "current"
32
+ workspace_path: str | None = None
33
+ unavailable_reason: str | None = None
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class ToolFile:
38
+ filename: str
39
+ media_type: str | None
40
+ data: bytes = field(repr=False)
41
+
42
+
43
+ class ToolFiles(Protocol):
44
+ """Available only when ``permissions.tool_files`` is declared.
45
+
46
+ Reads are bounded by the requested limit and host limits, and share an
47
+ aggregate byte budget per invocation. They use the caller's workspace and
48
+ admitted turn inputs; they never fetch arbitrary URLs or re-fetch Discord.
49
+ The port expires when the handler returns. Copying bytes does not extend
50
+ the host's privacy/retention guarantees; modules own any further persistence.
51
+ """
52
+
53
+ @property
54
+ def attachments(self) -> tuple[ToolAttachment, ...]: ...
55
+
56
+ async def read_attachment(self, attachment_id: str, *, max_bytes: int) -> ToolFile: ...
57
+
58
+ async def read_workspace(self, path: str, *, max_bytes: int) -> ToolFile: ...
59
+
60
+
61
+ __all__ = ["FileAccessError", "ToolAttachment", "ToolFile", "ToolFiles"]
@@ -23,6 +23,7 @@ from typing import Any, TypeVar, overload
23
23
  from pydantic_settings import BaseSettings
24
24
 
25
25
  from kimi_agent_module_api.trust import TrustTier
26
+ from kimi_agent_module_api.files import FileAccessError, ToolAttachment, ToolFile
26
27
 
27
28
  from kimi_agent_module_api.contracts import (
28
29
  ALL_DISCORD_ACTIONS,
@@ -1313,6 +1314,70 @@ def load_context(
1313
1314
  return context, recorder
1314
1315
 
1315
1316
 
1317
+ class FakeToolFiles:
1318
+ """In-memory invocation files; paths and IDs are exact fixture keys.
1319
+
1320
+ ``close()`` simulates handler return. Bounds and aggregate accounting match
1321
+ the host. This fake never reads the test process's filesystem or network.
1322
+ """
1323
+
1324
+ def __init__(
1325
+ self,
1326
+ attachments: tuple[ToolAttachment, ...] = (),
1327
+ *,
1328
+ attachment_files: Mapping[str, ToolFile] | None = None,
1329
+ workspace_files: Mapping[str, ToolFile] | None = None,
1330
+ max_bytes: int = 50 * 1024 * 1024,
1331
+ ) -> None:
1332
+ self._attachments = attachments
1333
+ self.attachment_files = dict(attachment_files or {})
1334
+ self.workspace_files = dict(workspace_files or {})
1335
+ self.reads: list[tuple[str, str, int]] = []
1336
+ self._remaining = min(max_bytes, 50 * 1024 * 1024)
1337
+ self._active = True
1338
+
1339
+ def close(self) -> None:
1340
+ self._active = False
1341
+
1342
+ def _check_active(self) -> None:
1343
+ if not self._active:
1344
+ raise FileAccessError("expired", "File access expired when the tool invocation ended.")
1345
+
1346
+ @property
1347
+ def attachments(self) -> tuple[ToolAttachment, ...]:
1348
+ self._check_active()
1349
+ return self._attachments
1350
+
1351
+ def _read(self, file: ToolFile | None, max_bytes: int) -> ToolFile:
1352
+ self._check_active()
1353
+ if type(max_bytes) is not int or max_bytes < 1:
1354
+ raise FileAccessError("invalid_limit", "max_bytes must be a positive integer.")
1355
+ if self._remaining <= 0:
1356
+ raise FileAccessError(
1357
+ "budget_exhausted", "The invocation's file read budget is exhausted."
1358
+ )
1359
+ if file is None:
1360
+ raise FileAccessError("unavailable", "File is not available in this fixture.")
1361
+ if len(file.data) > min(max_bytes, self._remaining):
1362
+ raise FileAccessError("too_large", "File exceeds the file read limit.")
1363
+ self._remaining -= len(file.data)
1364
+ return file
1365
+
1366
+ async def read_attachment(self, attachment_id: str, *, max_bytes: int) -> ToolFile:
1367
+ self._check_active()
1368
+ entry = next((item for item in self.attachments if item.id == attachment_id), None)
1369
+ if entry is None:
1370
+ raise FileAccessError("unknown_attachment", "Unknown attachment for this invocation.")
1371
+ if entry.unavailable_reason:
1372
+ raise FileAccessError("unavailable", entry.unavailable_reason)
1373
+ self.reads.append(("attachment", attachment_id, max_bytes))
1374
+ return self._read(self.attachment_files.get(attachment_id), max_bytes)
1375
+
1376
+ async def read_workspace(self, path: str, *, max_bytes: int) -> ToolFile:
1377
+ self.reads.append(("workspace", path, max_bytes))
1378
+ return self._read(self.workspace_files.get(path), max_bytes)
1379
+
1380
+
1316
1381
  __all__ = [
1317
1382
  "DiscordCall",
1318
1383
  "FakeDiscordActions",
@@ -1327,6 +1392,7 @@ __all__ = [
1327
1392
  "FakeResponse",
1328
1393
  "FakeScheduler",
1329
1394
  "FakeServiceRegistry",
1395
+ "FakeToolFiles",
1330
1396
  "FakeTrust",
1331
1397
  "LoadContextRecorder",
1332
1398
  "MemoryStorage",
@@ -7,6 +7,7 @@ from dataclasses import dataclass, field
7
7
  from typing import Any, Protocol
8
8
 
9
9
  from kimi_agent_module_api.trust import TrustTier
10
+ from kimi_agent_module_api.files import ToolFiles
10
11
 
11
12
 
12
13
  @dataclass(frozen=True, slots=True)
@@ -32,6 +33,9 @@ class ModuleToolContext:
32
33
  # Discord message that initiated this model turn. It is absent for personal
33
34
  # app commands and other surfaces that are not rooted in a Discord message.
34
35
  trigger_discord_message_id: int | None = None
36
+ # Invocation-scoped read-only port. Declare permissions.tool_files and
37
+ # require tools.files.v1; never retain this port beyond the handler.
38
+ files: ToolFiles | None = None
35
39
 
36
40
 
37
41
  type ModuleToolHandler = Callable[[dict[str, Any], ModuleToolContext], Coroutine[Any, Any, str]]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kimi-agent-module-api
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: Stable contracts for community-built assistant modules
5
5
  Author: Webhead
6
6
  License-Expression: MIT
@@ -30,6 +30,8 @@ implementation, or module loader. It exports:
30
30
  (storage, scheduler, events, Discord actions, interactions, HTTP, services,
31
31
  trust, proposals, health) plus the validators the host runs at preflight.
32
32
  - `kimi_agent_module_api.events`: the normalized `discord.*` event payloads.
33
+ - `kimi_agent_module_api.files`: `ToolFiles`, `ToolAttachment`, `ToolFile`, and
34
+ `FileAccessError` for invocation-scoped attachment and workspace reads.
33
35
  - `kimi_agent_module_api.testing`: a fake for every port, `load_context()` for
34
36
  exercising `create()`, and `MemoryStorage` (install the `testing` extra) so a
35
37
  module can unit test itself with only this package installed.
@@ -91,6 +93,18 @@ calls receive the exact source Discord message snowflake; other surfaces receive
91
93
  Modules use namespaced guild documents and the physical table names returned
92
94
  by `ctx.storage.table()`.
93
95
 
96
+ SDK 2.2 adds optional `ModuleToolContext.files`. Declare
97
+ `ModulePermissions(tool_files=True)`, depend on `kimi-agent-module-api>=2.2,<3`,
98
+ and require `tools.files.v1` in `ModuleSpec.requires_capabilities`. Existing modules
99
+ keep `api_version=2` and receive `files=None` without the permission.
100
+ `ctx.files.attachments` lists admitted current/reply entries;
101
+ `read_attachment(id, max_bytes=...)` and `read_workspace(path, max_bytes=...)`
102
+ return bounded bytes without network downloads. The reader expires when the handler
103
+ returns and is scoped to the actual caller. `testing.FakeToolFiles` supports
104
+ independent tests. See the
105
+ [file access guide](https://github.com/webhead2oo9/kimi-agent/blob/main/docs/module-files.md)
106
+ for moderation, reply-image availability, privacy, and limits.
107
+
94
108
  ## Testing the SDK
95
109
 
96
110
  From this package directory, run its tests without installing the Kimi application:
@@ -4,6 +4,7 @@ pyproject.toml
4
4
  src/kimi_agent_module_api/__init__.py
5
5
  src/kimi_agent_module_api/contracts.py
6
6
  src/kimi_agent_module_api/events.py
7
+ src/kimi_agent_module_api/files.py
7
8
  src/kimi_agent_module_api/images.py
8
9
  src/kimi_agent_module_api/py.typed
9
10
  src/kimi_agent_module_api/settings.py
@@ -16,5 +17,6 @@ src/kimi_agent_module_api.egg-info/dependency_links.txt
16
17
  src/kimi_agent_module_api.egg-info/requires.txt
17
18
  src/kimi_agent_module_api.egg-info/top_level.txt
18
19
  tests/test_contracts.py
20
+ tests/test_files.py
19
21
  tests/test_public_api.py
20
22
  tests/test_testing.py
@@ -83,8 +83,8 @@ def test_spec_and_runtime_context_keep_stable_defaults() -> None:
83
83
  assert {"events", "scheduler", "storage", "discord", "interactions", "services"} <= required
84
84
 
85
85
 
86
- def test_distribution_version_includes_trigger_message_contract() -> None:
87
- assert version("kimi-agent-module-api") == "2.1.0"
86
+ def test_distribution_version_includes_tool_files_contract() -> None:
87
+ assert version("kimi-agent-module-api") == "2.2.0"
88
88
 
89
89
 
90
90
  def test_spec_requires_an_explicit_keyword_api_version() -> None:
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from kimi_agent_module_api import FileAccessError, ToolAttachment, ToolFile, ToolFiles
6
+ from kimi_agent_module_api.testing import FakeToolFiles
7
+
8
+
9
+ @pytest.mark.asyncio
10
+ async def test_fake_preserves_bytes_and_enforces_aggregate_budget() -> None:
11
+ attachment = ToolAttachment("a", "image.png", 3, "image/png")
12
+ file = ToolFile("image.png", "image/png", b"raw")
13
+ fake = FakeToolFiles(
14
+ (attachment,),
15
+ attachment_files={"a": file},
16
+ workspace_files={"saved.png": file},
17
+ max_bytes=5,
18
+ )
19
+ port: ToolFiles = fake
20
+ assert (await port.read_attachment("a", max_bytes=3)).data == b"raw"
21
+ with pytest.raises(FileAccessError, match="limit"):
22
+ await port.read_workspace("saved.png", max_bytes=3)
23
+ assert "raw" not in repr(file)
24
+ fake.close()
25
+ with pytest.raises(FileAccessError) as error:
26
+ await port.read_attachment("a", max_bytes=3)
27
+ assert error.value.code == "expired"
28
+
29
+
30
+ @pytest.mark.asyncio
31
+ async def test_fake_does_not_read_unavailable_attachments() -> None:
32
+ fake = FakeToolFiles(
33
+ (ToolAttachment("a", "audio.wav", 3, "audio/wav", unavailable_reason="Blocked"),),
34
+ attachment_files={"a": ToolFile("audio.wav", "audio/wav", b"raw")},
35
+ )
36
+ with pytest.raises(FileAccessError, match="Blocked"):
37
+ await fake.read_attachment("a", max_bytes=3)
38
+ with pytest.raises(FileAccessError) as error:
39
+ await fake.read_attachment("other", max_bytes=3)
40
+ assert error.value.code == "unknown_attachment"
41
+ assert not fake.reads