klickd 3.0.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.
- klickd/__init__.py +35 -0
- klickd/_types.py +84 -0
- klickd/decode.py +181 -0
- klickd/encode.py +116 -0
- klickd/errors.py +32 -0
- klickd-3.0.0.dist-info/METADATA +138 -0
- klickd-3.0.0.dist-info/RECORD +8 -0
- klickd-3.0.0.dist-info/WHEEL +4 -0
klickd/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# klickd — Official Python library for .klickd portable AI context files
|
|
2
|
+
# SPDX-License-Identifier: CC0-1.0
|
|
3
|
+
#
|
|
4
|
+
# One soul. Any model. Any body.
|
|
5
|
+
#
|
|
6
|
+
# Repository: https://github.com/Davincc77/klickdskill
|
|
7
|
+
# DOI: https://doi.org/10.5281/zenodo.20262530
|
|
8
|
+
|
|
9
|
+
from .decode import load_klickd
|
|
10
|
+
from .encode import save_klickd
|
|
11
|
+
from .errors import KlickdError, KlickdErrorCode, HTTP_STATUS
|
|
12
|
+
from ._types import (
|
|
13
|
+
KlickdPayload,
|
|
14
|
+
KlickdEnvelope,
|
|
15
|
+
KlickdMemoryEntry,
|
|
16
|
+
KlickdIdentity,
|
|
17
|
+
KlickdContext,
|
|
18
|
+
KlickdKnowledge,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__version__ = "3.0.0"
|
|
22
|
+
__all__ = [
|
|
23
|
+
"load_klickd",
|
|
24
|
+
"save_klickd",
|
|
25
|
+
"KlickdError",
|
|
26
|
+
"KlickdErrorCode",
|
|
27
|
+
"HTTP_STATUS",
|
|
28
|
+
"KlickdPayload",
|
|
29
|
+
"KlickdEnvelope",
|
|
30
|
+
"KlickdMemoryEntry",
|
|
31
|
+
"KlickdIdentity",
|
|
32
|
+
"KlickdContext",
|
|
33
|
+
"KlickdKnowledge",
|
|
34
|
+
"__version__",
|
|
35
|
+
]
|
klickd/_types.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# .klickd v3 — Python TypedDict definitions
|
|
2
|
+
# SPDX-License-Identifier: CC0-1.0
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any, List, Literal, Optional
|
|
7
|
+
from typing_extensions import TypedDict, Required
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class KlickdKdfArgon2idParams(TypedDict):
|
|
11
|
+
m: int
|
|
12
|
+
t: int
|
|
13
|
+
p: int
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class KlickdKdfArgon2id(TypedDict):
|
|
17
|
+
name: Literal["argon2id"]
|
|
18
|
+
params: KlickdKdfArgon2idParams
|
|
19
|
+
salt: str # base64
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class KlickdKdfPbkdf2Params(TypedDict):
|
|
23
|
+
iterations: int
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class KlickdKdfPbkdf2(TypedDict):
|
|
27
|
+
name: Literal["pbkdf2-sha256"]
|
|
28
|
+
params: KlickdKdfPbkdf2Params
|
|
29
|
+
salt: str # base64
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class KlickdCipher(TypedDict):
|
|
33
|
+
name: Literal["AES-256-GCM"]
|
|
34
|
+
iv: str # base64
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class KlickdEnvelope(TypedDict, total=False):
|
|
38
|
+
klickd_version: Required[str]
|
|
39
|
+
encrypted: Required[bool]
|
|
40
|
+
domain: Required[str]
|
|
41
|
+
created_at: Required[str] # RFC 3339 UTC Z
|
|
42
|
+
kdf: Required[dict] # KlickdKdfArgon2id | KlickdKdfPbkdf2
|
|
43
|
+
cipher: Required[KlickdCipher]
|
|
44
|
+
ciphertext: Required[str] # base64
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class KlickdMemoryEntry(TypedDict, total=False):
|
|
48
|
+
id: Required[str] # UUID v4
|
|
49
|
+
ts: Required[str] # RFC 3339 UTC Z
|
|
50
|
+
role: Required[str] # user | assistant | system
|
|
51
|
+
content: Required[str]
|
|
52
|
+
modality: Required[str] # text | image | audio | tool_call
|
|
53
|
+
tags: List[str]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class KlickdIdentity(TypedDict, total=False):
|
|
57
|
+
name: str
|
|
58
|
+
language: str
|
|
59
|
+
timezone: str
|
|
60
|
+
communication_style: str
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class KlickdContext(TypedDict, total=False):
|
|
64
|
+
current_state: str
|
|
65
|
+
decisions_locked: List[str]
|
|
66
|
+
artifacts: List[Any]
|
|
67
|
+
summary: str
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class KlickdKnowledge(TypedDict, total=False):
|
|
71
|
+
mastered: List[str]
|
|
72
|
+
gaps: List[str]
|
|
73
|
+
next_steps: List[str]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class KlickdPayload(TypedDict, total=False):
|
|
77
|
+
payload_schema_version: Required[str]
|
|
78
|
+
domain_schema_version: Required[str]
|
|
79
|
+
identity: KlickdIdentity
|
|
80
|
+
agent_instructions: str
|
|
81
|
+
user_preferences: dict
|
|
82
|
+
context: KlickdContext
|
|
83
|
+
knowledge: KlickdKnowledge
|
|
84
|
+
memory: List[KlickdMemoryEntry]
|
klickd/decode.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# .klickd v3 — decode (load) implementation
|
|
2
|
+
# SPDX-License-Identifier: CC0-1.0
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import base64
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import jcs
|
|
12
|
+
from argon2.low_level import hash_secret_raw, Type
|
|
13
|
+
from cryptography.exceptions import InvalidTag
|
|
14
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
15
|
+
|
|
16
|
+
from .errors import KlickdError, KlickdErrorCode
|
|
17
|
+
|
|
18
|
+
SUPPORTED_MAJOR = 3
|
|
19
|
+
REQUIRED_ENVELOPE_FIELDS = frozenset(
|
|
20
|
+
["klickd_version", "encrypted", "domain", "created_at", "kdf", "cipher", "ciphertext"]
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _b64_decode(s: str) -> bytes:
|
|
25
|
+
"""RFC 4648 §4 standard padded base64 decode."""
|
|
26
|
+
return base64.b64decode(s)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _derive_key_argon2id(passphrase: str, salt: bytes, m: int, t: int, p: int) -> bytes:
|
|
30
|
+
return hash_secret_raw(
|
|
31
|
+
secret=passphrase.encode("utf-8"),
|
|
32
|
+
salt=salt,
|
|
33
|
+
time_cost=t,
|
|
34
|
+
memory_cost=m,
|
|
35
|
+
parallelism=p,
|
|
36
|
+
hash_len=32,
|
|
37
|
+
type=Type.ID,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _derive_key_pbkdf2(passphrase: str, salt: bytes, iterations: int) -> bytes:
|
|
42
|
+
return hashlib.pbkdf2_hmac("sha256", passphrase.encode("utf-8"), salt, iterations, dklen=32)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def load_klickd(
|
|
46
|
+
file_bytes: bytes,
|
|
47
|
+
passphrase: str | None = None,
|
|
48
|
+
legacy: bool = False,
|
|
49
|
+
) -> dict[str, Any]:
|
|
50
|
+
"""
|
|
51
|
+
Decrypt and parse a .klickd envelope.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
file_bytes: Raw .klickd file content (UTF-8 JSON bytes).
|
|
55
|
+
passphrase: Decryption passphrase.
|
|
56
|
+
legacy: Set ``True`` to allow legacy PBKDF2-SHA256/600k v2.x files.
|
|
57
|
+
Default: ``False``.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
The decrypted payload as a dict (KlickdPayload-compatible).
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
KlickdError: On any format, version, authentication, or schema error.
|
|
64
|
+
"""
|
|
65
|
+
# Parse envelope JSON
|
|
66
|
+
try:
|
|
67
|
+
envelope: dict[str, Any] = json.loads(file_bytes.decode("utf-8"))
|
|
68
|
+
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
|
|
69
|
+
raise KlickdError(KlickdErrorCode.FORMAT, f"Invalid JSON envelope: {exc}") from exc
|
|
70
|
+
|
|
71
|
+
if not isinstance(envelope, dict):
|
|
72
|
+
raise KlickdError(KlickdErrorCode.FORMAT, "Envelope must be a JSON object.")
|
|
73
|
+
|
|
74
|
+
# Validate required fields
|
|
75
|
+
missing = REQUIRED_ENVELOPE_FIELDS - envelope.keys()
|
|
76
|
+
if missing:
|
|
77
|
+
raise KlickdError(
|
|
78
|
+
KlickdErrorCode.FORMAT,
|
|
79
|
+
f"Missing required envelope fields: {', '.join(sorted(missing))}",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Check major version
|
|
83
|
+
try:
|
|
84
|
+
major = int(str(envelope["klickd_version"]).split(".")[0])
|
|
85
|
+
except (ValueError, IndexError) as exc:
|
|
86
|
+
raise KlickdError(
|
|
87
|
+
KlickdErrorCode.VERSION,
|
|
88
|
+
f"Cannot parse klickd_version: {envelope['klickd_version']}",
|
|
89
|
+
) from exc
|
|
90
|
+
|
|
91
|
+
if major != SUPPORTED_MAJOR:
|
|
92
|
+
raise KlickdError(
|
|
93
|
+
KlickdErrorCode.VERSION,
|
|
94
|
+
f"Unsupported klickd_version major: {envelope['klickd_version']}. "
|
|
95
|
+
f"This library supports v{SUPPORTED_MAJOR}.x.",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Reconstruct AAD: JCS over the 6 canonical fields
|
|
99
|
+
envelope_for_aad: dict[str, Any] = {
|
|
100
|
+
"klickd_version": envelope["klickd_version"],
|
|
101
|
+
"encrypted": envelope["encrypted"],
|
|
102
|
+
"domain": envelope["domain"],
|
|
103
|
+
"created_at": envelope["created_at"],
|
|
104
|
+
"kdf": envelope["kdf"],
|
|
105
|
+
"cipher": envelope["cipher"],
|
|
106
|
+
}
|
|
107
|
+
aad: bytes = jcs.canonicalize(envelope_for_aad)
|
|
108
|
+
|
|
109
|
+
# Require passphrase
|
|
110
|
+
if not passphrase:
|
|
111
|
+
raise KlickdError(
|
|
112
|
+
KlickdErrorCode.AUTH,
|
|
113
|
+
"A passphrase is required to decrypt this file.",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Derive key
|
|
117
|
+
kdf = envelope["kdf"]
|
|
118
|
+
kdf_name: str = kdf.get("name", "")
|
|
119
|
+
|
|
120
|
+
if kdf_name == "argon2id":
|
|
121
|
+
try:
|
|
122
|
+
salt = _b64_decode(kdf["salt"])
|
|
123
|
+
params = kdf["params"]
|
|
124
|
+
key = _derive_key_argon2id(passphrase, salt, params["m"], params["t"], params["p"])
|
|
125
|
+
except (KeyError, TypeError) as exc:
|
|
126
|
+
raise KlickdError(KlickdErrorCode.FORMAT, f"Invalid Argon2id KDF params: {exc}") from exc
|
|
127
|
+
|
|
128
|
+
elif kdf_name == "pbkdf2-sha256":
|
|
129
|
+
if not legacy:
|
|
130
|
+
raise KlickdError(
|
|
131
|
+
KlickdErrorCode.KDF,
|
|
132
|
+
"Legacy PBKDF2 KDF detected. Set legacy=True to enable reading legacy v2.x files.",
|
|
133
|
+
)
|
|
134
|
+
try:
|
|
135
|
+
salt = _b64_decode(kdf["salt"])
|
|
136
|
+
iterations = kdf["params"]["iterations"]
|
|
137
|
+
key = _derive_key_pbkdf2(passphrase, salt, iterations)
|
|
138
|
+
except (KeyError, TypeError) as exc:
|
|
139
|
+
raise KlickdError(KlickdErrorCode.FORMAT, f"Invalid PBKDF2 KDF params: {exc}") from exc
|
|
140
|
+
|
|
141
|
+
else:
|
|
142
|
+
raise KlickdError(KlickdErrorCode.KDF, f"Unknown KDF: '{kdf_name}'.")
|
|
143
|
+
|
|
144
|
+
# Decode ciphertext (ciphertext || 16-byte GCM tag, per AESGCM convention)
|
|
145
|
+
try:
|
|
146
|
+
ciphertext_with_tag = _b64_decode(envelope["ciphertext"])
|
|
147
|
+
except Exception as exc:
|
|
148
|
+
raise KlickdError(KlickdErrorCode.FORMAT, f"Invalid ciphertext base64: {exc}") from exc
|
|
149
|
+
|
|
150
|
+
if len(ciphertext_with_tag) < 16:
|
|
151
|
+
raise KlickdError(KlickdErrorCode.FORMAT, "Ciphertext too short.")
|
|
152
|
+
|
|
153
|
+
# Decrypt AES-256-GCM
|
|
154
|
+
try:
|
|
155
|
+
iv = _b64_decode(envelope["cipher"]["iv"])
|
|
156
|
+
except (KeyError, Exception) as exc:
|
|
157
|
+
raise KlickdError(KlickdErrorCode.FORMAT, f"Invalid cipher IV: {exc}") from exc
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
aesgcm = AESGCM(key)
|
|
161
|
+
plaintext = aesgcm.decrypt(iv, ciphertext_with_tag, aad)
|
|
162
|
+
except InvalidTag as exc:
|
|
163
|
+
raise KlickdError(
|
|
164
|
+
KlickdErrorCode.AUTH,
|
|
165
|
+
"Decryption failed: wrong passphrase or corrupted file.",
|
|
166
|
+
) from exc
|
|
167
|
+
|
|
168
|
+
# Parse payload JSON
|
|
169
|
+
try:
|
|
170
|
+
payload: dict[str, Any] = json.loads(plaintext.decode("utf-8"))
|
|
171
|
+
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
|
|
172
|
+
raise KlickdError(KlickdErrorCode.FORMAT, f"Decrypted data is not valid JSON: {exc}") from exc
|
|
173
|
+
|
|
174
|
+
# Validate payload_schema_version
|
|
175
|
+
if not payload.get("payload_schema_version"):
|
|
176
|
+
raise KlickdError(
|
|
177
|
+
KlickdErrorCode.SCHEMA,
|
|
178
|
+
"Decrypted payload is missing payload_schema_version.",
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return payload
|
klickd/encode.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# .klickd v3 — encode (save) implementation
|
|
2
|
+
# SPDX-License-Identifier: CC0-1.0
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import base64
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import jcs
|
|
12
|
+
from argon2.low_level import hash_secret_raw, Type
|
|
13
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
14
|
+
|
|
15
|
+
from .errors import KlickdError, KlickdErrorCode
|
|
16
|
+
|
|
17
|
+
KLICKD_VERSION = "3.0.0"
|
|
18
|
+
DEFAULT_KDF_PARAMS = {"m": 65536, "t": 3, "p": 1}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _b64_encode(data: bytes) -> str:
|
|
22
|
+
"""RFC 4648 §4 standard padded base64."""
|
|
23
|
+
return base64.b64encode(data).decode("ascii")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _derive_key_argon2id(passphrase: str, salt: bytes, m: int, t: int, p: int) -> bytes:
|
|
27
|
+
"""Derive a 32-byte key using Argon2id."""
|
|
28
|
+
return hash_secret_raw(
|
|
29
|
+
secret=passphrase.encode("utf-8"),
|
|
30
|
+
salt=salt,
|
|
31
|
+
time_cost=t,
|
|
32
|
+
memory_cost=m,
|
|
33
|
+
parallelism=p,
|
|
34
|
+
hash_len=32,
|
|
35
|
+
type=Type.ID,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def save_klickd(
|
|
40
|
+
payload: dict[str, Any],
|
|
41
|
+
passphrase: str,
|
|
42
|
+
domain: str = "education",
|
|
43
|
+
kdf_params: dict[str, int] | None = None,
|
|
44
|
+
) -> bytes:
|
|
45
|
+
"""
|
|
46
|
+
Encrypt a KlickdPayload dict and return a .klickd JSON envelope as UTF-8 bytes.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
payload: Dict conforming to KlickdPayload schema.
|
|
50
|
+
Must include ``payload_schema_version``.
|
|
51
|
+
passphrase: Encryption passphrase (minimum 8 characters).
|
|
52
|
+
domain: .klickd domain tag. Default: ``"education"``.
|
|
53
|
+
kdf_params: Argon2id parameter overrides.
|
|
54
|
+
Defaults to ``{"m": 65536, "t": 3, "p": 1}``.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
UTF-8 bytes of the complete .klickd JSON envelope.
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
KlickdError: On validation or cryptographic failure.
|
|
61
|
+
"""
|
|
62
|
+
params = kdf_params or DEFAULT_KDF_PARAMS
|
|
63
|
+
|
|
64
|
+
# Validate passphrase
|
|
65
|
+
if not passphrase or len(passphrase) < 8:
|
|
66
|
+
raise KlickdError(KlickdErrorCode.WEAK_PASS, "Passphrase must be at least 8 characters.")
|
|
67
|
+
|
|
68
|
+
# Validate payload schema version
|
|
69
|
+
if not payload.get("payload_schema_version"):
|
|
70
|
+
raise KlickdError(KlickdErrorCode.SCHEMA, "payload_schema_version is required.")
|
|
71
|
+
|
|
72
|
+
# Generate fresh CSPRNG salt (16 bytes) and IV (12 bytes)
|
|
73
|
+
salt = os.urandom(16)
|
|
74
|
+
iv = os.urandom(12)
|
|
75
|
+
|
|
76
|
+
# Derive 32-byte key
|
|
77
|
+
key = _derive_key_argon2id(
|
|
78
|
+
passphrase, salt, params["m"], params["t"], params["p"]
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Build the 6-field envelope object for AAD (without ciphertext)
|
|
82
|
+
from datetime import datetime, timezone
|
|
83
|
+
created_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
84
|
+
|
|
85
|
+
envelope_for_aad: dict[str, Any] = {
|
|
86
|
+
"klickd_version": KLICKD_VERSION,
|
|
87
|
+
"encrypted": True,
|
|
88
|
+
"domain": domain,
|
|
89
|
+
"created_at": created_at,
|
|
90
|
+
"kdf": {
|
|
91
|
+
"name": "argon2id",
|
|
92
|
+
"params": {"m": params["m"], "t": params["t"], "p": params["p"]},
|
|
93
|
+
"salt": _b64_encode(salt),
|
|
94
|
+
},
|
|
95
|
+
"cipher": {
|
|
96
|
+
"name": "AES-256-GCM",
|
|
97
|
+
"iv": _b64_encode(iv),
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# AAD = RFC 8785 JCS (JSON Canonicalization Scheme)
|
|
102
|
+
aad: bytes = jcs.canonicalize(envelope_for_aad)
|
|
103
|
+
|
|
104
|
+
# Encrypt payload JSON with AES-256-GCM
|
|
105
|
+
# cryptography's AESGCM appends the 16-byte tag to ciphertext
|
|
106
|
+
aesgcm = AESGCM(key)
|
|
107
|
+
plaintext = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
|
108
|
+
ciphertext_with_tag = aesgcm.encrypt(iv, plaintext, aad)
|
|
109
|
+
|
|
110
|
+
# Build final envelope
|
|
111
|
+
envelope: dict[str, Any] = {
|
|
112
|
+
**envelope_for_aad,
|
|
113
|
+
"ciphertext": _b64_encode(ciphertext_with_tag),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return json.dumps(envelope, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
klickd/errors.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# .klickd error definitions
|
|
2
|
+
# SPDX-License-Identifier: CC0-1.0
|
|
3
|
+
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class KlickdErrorCode(str, Enum):
|
|
8
|
+
AUTH = "KLICKD_E_AUTH" # Wrong passphrase / GCM tag mismatch
|
|
9
|
+
VERSION = "KLICKD_E_VERSION" # Unsupported klickd_version major
|
|
10
|
+
FORMAT = "KLICKD_E_FORMAT" # Malformed envelope JSON / missing fields
|
|
11
|
+
KDF = "KLICKD_E_KDF" # Unknown/unsupported KDF
|
|
12
|
+
WEAK_PASS = "KLICKD_E_WEAK_PASS" # Passphrase < 8 characters
|
|
13
|
+
SCHEMA = "KLICKD_E_SCHEMA" # Missing payload_schema_version
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
HTTP_STATUS: dict[KlickdErrorCode, int] = {
|
|
17
|
+
KlickdErrorCode.AUTH: 401,
|
|
18
|
+
KlickdErrorCode.VERSION: 400,
|
|
19
|
+
KlickdErrorCode.FORMAT: 400,
|
|
20
|
+
KlickdErrorCode.KDF: 400,
|
|
21
|
+
KlickdErrorCode.WEAK_PASS: 422,
|
|
22
|
+
KlickdErrorCode.SCHEMA: 400,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class KlickdError(Exception):
|
|
27
|
+
"""Raised for all .klickd format and cryptographic errors."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, code: KlickdErrorCode, message: str) -> None:
|
|
30
|
+
super().__init__(f"{code}: {message}")
|
|
31
|
+
self.code = code
|
|
32
|
+
self.http_status: int = HTTP_STATUS[code]
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: klickd
|
|
3
|
+
Version: 3.0.0
|
|
4
|
+
Summary: Official Python library for reading and writing .klickd portable AI context files
|
|
5
|
+
Project-URL: Homepage, https://klickd.app
|
|
6
|
+
Project-URL: Repository, https://github.com/Davincc77/klickdskill
|
|
7
|
+
Project-URL: Documentation, https://github.com/Davincc77/klickdskill/blob/main/SPEC.md
|
|
8
|
+
Author-email: "Vince C." <Luxlearn@pm.me>
|
|
9
|
+
License: CC0-1.0
|
|
10
|
+
Keywords: ai-context,encrypted,klickd,memory,portable
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Security :: Cryptography
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: argon2-cffi>=23.1
|
|
23
|
+
Requires-Dist: cryptography>=41.0
|
|
24
|
+
Requires-Dist: jcs>=0.2
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# klickd
|
|
28
|
+
|
|
29
|
+
Official Python library for reading and writing `.klickd` portable AI context files.
|
|
30
|
+
|
|
31
|
+
**One soul. Any model. Any body.**
|
|
32
|
+
|
|
33
|
+
[](https://pypi.org/project/klickd/)
|
|
34
|
+
[](https://pypi.org/project/klickd/)
|
|
35
|
+
[](https://creativecommons.org/publicdomain/zero/1.0/)
|
|
36
|
+
[](https://doi.org/10.5281/zenodo.20262530)
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install klickd
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Quick start
|
|
49
|
+
|
|
50
|
+
### Load (decrypt) a `.klickd` file
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from klickd import load_klickd
|
|
54
|
+
|
|
55
|
+
with open("context.klickd", "rb") as f:
|
|
56
|
+
payload = load_klickd(f.read(), passphrase="my-passphrase")
|
|
57
|
+
|
|
58
|
+
print(payload["identity"]["name"])
|
|
59
|
+
print(payload["memory"])
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Save (encrypt) a `.klickd` file
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from klickd import save_klickd
|
|
66
|
+
|
|
67
|
+
payload = {
|
|
68
|
+
"payload_schema_version": "3.0.0",
|
|
69
|
+
"domain_schema_version": "1.0.0",
|
|
70
|
+
"identity": {"name": "Alice", "language": "en", "timezone": "Europe/Luxembourg"},
|
|
71
|
+
"agent_instructions": "Be concise.",
|
|
72
|
+
"memory": [],
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
klickd_bytes = save_klickd(payload, passphrase="my-passphrase", domain="education")
|
|
76
|
+
|
|
77
|
+
with open("context.klickd", "wb") as f:
|
|
78
|
+
f.write(klickd_bytes)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Legacy v2.x files (PBKDF2)
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
payload = load_klickd(file_bytes, passphrase="my-passphrase", legacy=True)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Cryptographic specification (v3.0)
|
|
90
|
+
|
|
91
|
+
| Parameter | Value |
|
|
92
|
+
|---------------|-----------------------------------------|
|
|
93
|
+
| KDF (default) | Argon2id — m=65536, t=3, p=1 |
|
|
94
|
+
| KDF (legacy) | PBKDF2-SHA256 / 600 000 iterations |
|
|
95
|
+
| Cipher | AES-256-GCM |
|
|
96
|
+
| AAD | RFC 8785 JCS over 6 canonical fields |
|
|
97
|
+
| Base64 | RFC 4648 §4 standard padded |
|
|
98
|
+
| Salt | 16 bytes (CSPRNG) |
|
|
99
|
+
| IV | 12 bytes (CSPRNG) |
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Error codes
|
|
104
|
+
|
|
105
|
+
| Code | HTTP | Meaning |
|
|
106
|
+
|-----------------------|------|------------------------------------------|
|
|
107
|
+
| `KLICKD_E_AUTH` | 401 | Wrong passphrase / GCM tag mismatch |
|
|
108
|
+
| `KLICKD_E_VERSION` | 400 | Unsupported `klickd_version` major |
|
|
109
|
+
| `KLICKD_E_FORMAT` | 400 | Malformed JSON envelope / missing fields |
|
|
110
|
+
| `KLICKD_E_KDF` | 400 | Unknown or unavailable KDF |
|
|
111
|
+
| `KLICKD_E_WEAK_PASS` | 422 | Passphrase shorter than 8 characters |
|
|
112
|
+
| `KLICKD_E_SCHEMA` | 400 | Missing `payload_schema_version` |
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from klickd import KlickdError, KlickdErrorCode
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
payload = load_klickd(data, passphrase="wrong")
|
|
119
|
+
except KlickdError as e:
|
|
120
|
+
print(e.code) # KlickdErrorCode.AUTH
|
|
121
|
+
print(e.http_status) # 401
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Links
|
|
127
|
+
|
|
128
|
+
- Specification: [SPEC.md](https://github.com/Davincc77/klickdskill/blob/main/SPEC.md)
|
|
129
|
+
- Repository: [github.com/Davincc77/klickdskill](https://github.com/Davincc77/klickdskill)
|
|
130
|
+
- DOI: [10.5281/zenodo.20262530](https://doi.org/10.5281/zenodo.20262530)
|
|
131
|
+
- Homepage: [klickd.app](https://klickd.app)
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
[CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/) — Public Domain Dedication.
|
|
138
|
+
Author: Vince C. (Luxlearn, Luxembourg)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
klickd/__init__.py,sha256=MDpYv_3ZzqKku92X8zTJZIp14FgH7dS8m1jbj9O7-js,818
|
|
2
|
+
klickd/_types.py,sha256=mV7OKseFIdLNKATRjCfUzbAfYxG554dTWKNHfUxoeTU,2075
|
|
3
|
+
klickd/decode.py,sha256=9yNNZVaRPUOdaNp0LEiw691Ou-tJzDO67y0ufSW19bs,6056
|
|
4
|
+
klickd/encode.py,sha256=mwtqReq0aQeTs5UG_k38MgvngZCoHjUGvw4nGLWTITo,3619
|
|
5
|
+
klickd/errors.py,sha256=BKHZRRA5aTbbE0MybNSkCVILp9ZifnkCkT4IOqzcCP8,1078
|
|
6
|
+
klickd-3.0.0.dist-info/METADATA,sha256=youPc90jkDfzApHPGHJx3yetUxRSzBNkmABTsN12TyY,4508
|
|
7
|
+
klickd-3.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
8
|
+
klickd-3.0.0.dist-info/RECORD,,
|