@tokensmind/agent-network 0.1.0 → 0.1.3
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.
- package/README.md +81 -7
- package/openclaw.plugin.json +2 -2
- package/package.json +18 -8
- package/skills/tokensmind-agent-network/SKILL.md +145 -0
- package/skills/tokensmind-agent-network/package.json +4 -0
- package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/action_support.py +3 -1
- package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/agent_network_runtime.py +61 -9
- package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/action-executor.js +12 -1
- package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/action-validation.js +5 -0
- package/skills/tokensmind-agent-network/scripts/lib/agent-actions.js +91 -0
- package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/api-client.js +14 -2
- package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/contact-action.js +1 -1
- package/skills/tokensmind-agent-network/scripts/lib/memory-actions.js +112 -0
- package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/runtime/connector.js +8 -19
- package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/runtime/credentialStore.js +60 -11
- package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/runtime/requestPolicy.js +0 -11
- package/skills/tokensmind-agent-network/scripts/lib/runtime/systemCredentialStore.js +245 -0
- package/skills/tokensmind-agent-network/scripts/lib/runtime/systemCredentialSupport.js +71 -0
- package/skills/tokensmind-agent-network/scripts/memory_actions.py +131 -0
- package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/python_runtime/connector.py +8 -8
- package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/python_runtime/credential_store.py +56 -12
- package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/python_runtime/request_policy.py +1 -13
- package/skills/tokensmind-agent-network/scripts/python_runtime/system_credential_store.py +250 -0
- package/src/action-executor.js +12 -1
- package/src/action-validation.js +5 -0
- package/src/agent-actions.js +53 -6
- package/src/api-client.js +14 -2
- package/src/contact-action.js +1 -1
- package/src/index.js +4 -2
- package/src/memory-actions.js +112 -0
- package/src/runtime/connector.js +8 -19
- package/src/runtime/credentialStore.js +60 -11
- package/src/runtime/requestPolicy.js +0 -11
- package/src/runtime/systemCredentialStore.js +143 -74
- package/src/runtime/systemCredentialSupport.js +71 -0
- package/skills/tokensmind-agent-network-runtime/SKILL.md +0 -55
- package/skills/tokensmind-agent-network-runtime/scripts/lib/agent-actions.js +0 -44
- package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/systemCredentialStore.js +0 -176
- package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/system_credential_store.py +0 -143
- /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/action_executor.py +0 -0
- /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/action_validation.py +0 -0
- /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/agent-network-runtime.mjs +0 -0
- /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/governance_actions.py +0 -0
- /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/action-context.js +0 -0
- /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/action-errors.js +0 -0
- /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/governance-actions.js +0 -0
- /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/messaging-actions.js +0 -0
- /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/runtime/browser.js +0 -0
- /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/runtime/constants.js +0 -0
- /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/runtime/crypto.js +0 -0
- /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/runtime/deviceAuthorization.js +0 -0
- /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/runtime/httpClient.js +0 -0
- /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/runtime/portableStore.js +0 -0
- /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/workflow-store.js +0 -0
- /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/python_runtime/__init__.py +0 -0
- /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/python_runtime/browser.py +0 -0
- /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/python_runtime/constants.py +0 -0
- /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/python_runtime/crypto.py +0 -0
- /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/python_runtime/device_authorization.py +0 -0
- /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/python_runtime/errors.py +0 -0
- /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/python_runtime/http_client.py +0 -0
- /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/python_runtime/portable_store.py +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from urllib.parse import quote, urlencode
|
|
3
|
+
|
|
4
|
+
from action_support import ActionState
|
|
5
|
+
from action_validation import require_text, text
|
|
6
|
+
|
|
7
|
+
MEMORY_KINDS = frozenset(("agent_experience", "matching_preference"))
|
|
8
|
+
MEMORY_POLARITIES = frozenset(("negative", "positive"))
|
|
9
|
+
MEMORY_SCOPES = frozenset(("contextual", "global"))
|
|
10
|
+
# platform_event is written by the service alone and is rejected from this client.
|
|
11
|
+
MEMORY_SOURCES = frozenset(("model_inferred", "user_explicit"))
|
|
12
|
+
MEMORY_STATUSES = frozenset(("active", "deleted", "pending_review", "rejected"))
|
|
13
|
+
MAX_INFERRED_CONFIDENCE = 0.6
|
|
14
|
+
DEFAULT_LIST_LIMIT = 20
|
|
15
|
+
MAX_LIST_LIMIT = 100
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _choice(data, field, allowed):
|
|
19
|
+
value = text(data.get(field)).lower()
|
|
20
|
+
if value not in allowed:
|
|
21
|
+
raise ActionState("input_required", "%s has an unsupported value." % field, fields=[field])
|
|
22
|
+
return value
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _confidence(value, source_type):
|
|
26
|
+
if value in (None, ""):
|
|
27
|
+
return MAX_INFERRED_CONFIDENCE if source_type == "model_inferred" else 1
|
|
28
|
+
try:
|
|
29
|
+
confidence = float(value)
|
|
30
|
+
except (TypeError, ValueError) as error:
|
|
31
|
+
raise ActionState("input_required", "confidence must be a number.", fields=["confidence"]) from error
|
|
32
|
+
invalid = not math.isfinite(confidence) or confidence <= 0 or confidence > 1
|
|
33
|
+
inferred_high = source_type == "model_inferred" and confidence > MAX_INFERRED_CONFIDENCE
|
|
34
|
+
if invalid or inferred_high:
|
|
35
|
+
raise ActionState(
|
|
36
|
+
"input_required",
|
|
37
|
+
"confidence must be above 0 and no more than 0.6 for inferred memory.",
|
|
38
|
+
fields=["confidence"],
|
|
39
|
+
)
|
|
40
|
+
return confidence
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _owned_agent(api):
|
|
44
|
+
agents = api.request("GET", "/agent-network-api/agents?mine=1")
|
|
45
|
+
if len(agents) > 1:
|
|
46
|
+
raise ActionState("failed", "The account has more than one Agent profile.", code="AGENT_ACCOUNT_INVARIANT", retryable=False)
|
|
47
|
+
if not agents:
|
|
48
|
+
raise ActionState("failed", "Create the account Agent profile first.", code="AGENT_PROFILE_REQUIRED", retryable=False)
|
|
49
|
+
return agents[0]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _agent_path(agent_id, suffix):
|
|
53
|
+
return "/agent-network-api/agents/%s/%s" % (quote(str(agent_id), safe=""), suffix)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _optional_text(data, field):
|
|
57
|
+
return text(data.get(field)) or None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _memory_body(data):
|
|
61
|
+
source_type = _choice(data, "sourceType", MEMORY_SOURCES)
|
|
62
|
+
scope = _choice(data, "scope", MEMORY_SCOPES) if data.get("scope") else "contextual"
|
|
63
|
+
context_text = "" if scope == "global" else require_text(data.get("contextText"), "contextText")
|
|
64
|
+
return {
|
|
65
|
+
"clientMemoryId": require_text(data.get("clientMemoryId"), "clientMemoryId"),
|
|
66
|
+
"confidence": _confidence(data.get("confidence"), source_type),
|
|
67
|
+
"contextText": context_text,
|
|
68
|
+
"conversationId": _optional_text(data, "conversationId"),
|
|
69
|
+
"expiresAt": _optional_text(data, "expiresAt"),
|
|
70
|
+
"kind": _choice(data, "kind", MEMORY_KINDS),
|
|
71
|
+
"polarity": _choice(data, "polarity", MEMORY_POLARITIES),
|
|
72
|
+
"requirementId": _optional_text(data, "requirementId"),
|
|
73
|
+
"scope": scope,
|
|
74
|
+
"sourceType": source_type,
|
|
75
|
+
"statement": require_text(data.get("statement"), "statement"),
|
|
76
|
+
"targetAgentId": _optional_text(data, "targetAgentId"),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _integer(value, field, minimum, maximum=None):
|
|
81
|
+
if isinstance(value, bool):
|
|
82
|
+
raise ActionState("input_required", "%s must be an integer." % field, fields=[field])
|
|
83
|
+
try:
|
|
84
|
+
number = float(value)
|
|
85
|
+
except (TypeError, ValueError) as error:
|
|
86
|
+
raise ActionState("input_required", "%s must be an integer." % field, fields=[field]) from error
|
|
87
|
+
if not number.is_integer() or number < minimum or (maximum is not None and number > maximum):
|
|
88
|
+
raise ActionState("input_required", "%s must be an integer in range." % field, fields=[field])
|
|
89
|
+
return int(number)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _list_query(data):
|
|
93
|
+
limit = _integer(data.get("limit", DEFAULT_LIST_LIMIT), "limit", 1, MAX_LIST_LIMIT)
|
|
94
|
+
params = {"limit": limit}
|
|
95
|
+
if data.get("offset") is not None:
|
|
96
|
+
params["offset"] = _integer(data.get("offset"), "offset", 0)
|
|
97
|
+
if data.get("status"):
|
|
98
|
+
params["status"] = _choice(data, "status", MEMORY_STATUSES)
|
|
99
|
+
return urlencode(params)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_memory_settings(api, _data, _key):
|
|
103
|
+
agent = _owned_agent(api)
|
|
104
|
+
return api.request("GET", _agent_path(agent["id"], "memory-settings"))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def propose_memory(api, data, key):
|
|
108
|
+
body = _memory_body(data)
|
|
109
|
+
agent = _owned_agent(api)
|
|
110
|
+
settings_path = _agent_path(agent["id"], "memory-settings")
|
|
111
|
+
settings = api.request("GET", settings_path)
|
|
112
|
+
if settings.get("collectionEnabled") is not True:
|
|
113
|
+
raise ActionState("failed", "Memory collection is disabled for this Agent.", code="MEMORY_COLLECTION_DISABLED", retryable=False)
|
|
114
|
+
return api.request(
|
|
115
|
+
"POST", _agent_path(agent["id"], "memories"), body,
|
|
116
|
+
key=key + ":memory:propose",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def list_memories(api, data, _key):
|
|
121
|
+
query = _list_query(data)
|
|
122
|
+
agent = _owned_agent(api)
|
|
123
|
+
path = _agent_path(agent["id"], "memories") + "?" + query
|
|
124
|
+
return api.request("GET", path)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def delete_memory(api, data, key):
|
|
128
|
+
memory_id = require_text(data.get("memoryId"), "memoryId")
|
|
129
|
+
agent = _owned_agent(api)
|
|
130
|
+
path = _agent_path(agent["id"], "memories") + "/" + quote(memory_id, safe="")
|
|
131
|
+
return api.request("DELETE", path, key=key + ":memory:delete")
|
|
@@ -12,11 +12,8 @@ from .errors import AgentNetworkHttpError
|
|
|
12
12
|
from .http_client import UNSET
|
|
13
13
|
from .request_policy import (
|
|
14
14
|
build_business_url,
|
|
15
|
-
is_public_discovery_request,
|
|
16
15
|
validate_business_request,
|
|
17
16
|
)
|
|
18
|
-
from action_support import AuthorizationRequired
|
|
19
|
-
|
|
20
17
|
INVALID_CREDENTIAL_CODES = frozenset((
|
|
21
18
|
"AGENT_CREDENTIAL_EXPIRED",
|
|
22
19
|
"AGENT_CREDENTIAL_INVALID",
|
|
@@ -38,6 +35,12 @@ def _is_http_error(error, codes):
|
|
|
38
35
|
return isinstance(error, AgentNetworkHttpError) and error.code in codes
|
|
39
36
|
|
|
40
37
|
|
|
38
|
+
def _requires_authorization(error):
|
|
39
|
+
return isinstance(error, AgentNetworkHttpError) and (
|
|
40
|
+
error.status == 401 or error.code in INVALID_CREDENTIAL_CODES
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
41
44
|
class AgentNetworkConnector:
|
|
42
45
|
def __init__(self, store, browser, base_url, *, http, client):
|
|
43
46
|
self._store = store
|
|
@@ -60,15 +63,13 @@ class AgentNetworkConnector:
|
|
|
60
63
|
active = self._store.read("active")
|
|
61
64
|
if pending:
|
|
62
65
|
return self._resume_pending(pending, active, operation)
|
|
63
|
-
if is_public_discovery_request(operation) and not reauthorize:
|
|
64
|
-
return self._execute_business_request(operation)
|
|
65
66
|
if not active or reauthorize:
|
|
66
67
|
active = self._authorize(operation)
|
|
67
68
|
return self._execute_and_finalize(self._store.read("pending"), active)
|
|
68
69
|
try:
|
|
69
70
|
return self._execute_business_request(operation, active["token"])
|
|
70
71
|
except AgentNetworkHttpError as error:
|
|
71
|
-
if
|
|
72
|
+
if not _requires_authorization(error):
|
|
72
73
|
raise
|
|
73
74
|
self._store.remove("active")
|
|
74
75
|
active = self._authorize(operation)
|
|
@@ -103,7 +104,6 @@ class AgentNetworkConnector:
|
|
|
103
104
|
)
|
|
104
105
|
self._store.write("pending", pending)
|
|
105
106
|
self._browser.open(pending["authorization"]["verificationUrl"])
|
|
106
|
-
raise AuthorizationRequired(pending["authorization"]["verificationUrl"])
|
|
107
107
|
if pending["phase"] == "authorizing":
|
|
108
108
|
return self._complete_authorization(pending)
|
|
109
109
|
return self._store.read("active")
|
|
@@ -151,6 +151,6 @@ class AgentNetworkConnector:
|
|
|
151
151
|
except AgentNetworkHttpError as error:
|
|
152
152
|
if error.status < 500 and error.status != 429:
|
|
153
153
|
self._store.remove("pending")
|
|
154
|
-
if error
|
|
154
|
+
if _requires_authorization(error):
|
|
155
155
|
self._store.remove("active")
|
|
156
156
|
raise
|
|
@@ -7,7 +7,10 @@ from .portable_store import (
|
|
|
7
7
|
_origin_namespace,
|
|
8
8
|
resolve_portable_state_dir,
|
|
9
9
|
)
|
|
10
|
-
from .system_credential_store import
|
|
10
|
+
from .system_credential_store import (
|
|
11
|
+
SystemCredentialStoreError,
|
|
12
|
+
resolve_system_credential_store,
|
|
13
|
+
)
|
|
11
14
|
|
|
12
15
|
AUTO_SECURE_STORE = object()
|
|
13
16
|
|
|
@@ -94,11 +97,50 @@ def _file_stores(options):
|
|
|
94
97
|
]
|
|
95
98
|
|
|
96
99
|
|
|
100
|
+
def _validate_system_records(system_store):
|
|
101
|
+
for name in RECORD_NAMES:
|
|
102
|
+
system_store.read(name)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _initialize_store(
|
|
106
|
+
system_store, fallback, fallback_dir, legacy_dirs, options, on_fallback):
|
|
107
|
+
record_dir = os.path.join(fallback_dir, _origin_namespace(options["origin"]))
|
|
108
|
+
legacy_sources = _file_stores({**options, "directories": legacy_dirs})
|
|
109
|
+
if system_store is None:
|
|
110
|
+
on_fallback({
|
|
111
|
+
"backend": None,
|
|
112
|
+
"directory": record_dir,
|
|
113
|
+
"reason": "No supported system credential store is available",
|
|
114
|
+
})
|
|
115
|
+
migrate_credential_stores(fallback, legacy_sources)
|
|
116
|
+
return fallback
|
|
117
|
+
try:
|
|
118
|
+
if not hasattr(system_store, "verify_availability"):
|
|
119
|
+
raise SystemCredentialStoreError(
|
|
120
|
+
"System credential store does not support availability verification",
|
|
121
|
+
)
|
|
122
|
+
system_store.verify_availability()
|
|
123
|
+
_validate_system_records(system_store)
|
|
124
|
+
sources = _file_stores({
|
|
125
|
+
**options, "directories": [fallback_dir] + legacy_dirs,
|
|
126
|
+
})
|
|
127
|
+
migrate_credential_stores(system_store, sources)
|
|
128
|
+
return system_store
|
|
129
|
+
except SystemCredentialStoreError as error:
|
|
130
|
+
on_fallback({
|
|
131
|
+
"backend": system_store.backend,
|
|
132
|
+
"directory": record_dir,
|
|
133
|
+
"reason": str(error),
|
|
134
|
+
})
|
|
135
|
+
migrate_credential_stores(fallback, legacy_sources)
|
|
136
|
+
return fallback
|
|
137
|
+
|
|
138
|
+
|
|
97
139
|
def create_credential_store(
|
|
98
140
|
origin, platform, *, state_dir=None, env=None, home_dir=None,
|
|
99
141
|
secure_store=AUTO_SECURE_STORE,
|
|
100
142
|
secure_store_resolver=resolve_system_credential_store,
|
|
101
|
-
command_runner=None):
|
|
143
|
+
command_runner=None, on_fallback=None):
|
|
102
144
|
environment = os.environ if env is None else env
|
|
103
145
|
if state_dir:
|
|
104
146
|
return PortableStore(
|
|
@@ -119,17 +161,19 @@ def create_credential_store(
|
|
|
119
161
|
system_store = secure_store_resolver(**resolver_options)
|
|
120
162
|
else:
|
|
121
163
|
system_store = secure_store
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
164
|
+
legacy_dirs = list(dict.fromkeys(
|
|
165
|
+
resolve_legacy_state_dirs(platform, environment, home_dir),
|
|
166
|
+
))
|
|
167
|
+
return _initialize_store(
|
|
168
|
+
system_store,
|
|
169
|
+
fallback,
|
|
170
|
+
fallback_dir,
|
|
171
|
+
legacy_dirs,
|
|
172
|
+
{
|
|
129
173
|
"origin": origin,
|
|
130
174
|
"platform": platform,
|
|
131
175
|
"env": environment,
|
|
132
176
|
"home_dir": home_dir,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
177
|
+
},
|
|
178
|
+
on_fallback or (lambda _event: None),
|
|
179
|
+
)
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
from urllib.parse import
|
|
1
|
+
from urllib.parse import urljoin, urlsplit, urlunsplit
|
|
2
2
|
|
|
3
3
|
from .constants import INTERNAL_PATH_PATTERNS, MUTATION_METHODS, SUPPORTED_METHODS
|
|
4
4
|
|
|
5
|
-
PUBLIC_AGENT_DIRECTORY_PATH = "/agent-network-api/agents"
|
|
6
|
-
|
|
7
|
-
|
|
8
5
|
def normalize_base_url(value):
|
|
9
6
|
parsed = urlsplit(str(value))
|
|
10
7
|
if parsed.scheme not in ("http", "https") or not parsed.hostname:
|
|
@@ -74,12 +71,3 @@ def build_business_url(base_url, path):
|
|
|
74
71
|
if _is_internal_path(parsed.path):
|
|
75
72
|
raise ValueError("This endpoint is not available through the ordinary-user connector")
|
|
76
73
|
return url
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def is_public_discovery_request(operation):
|
|
80
|
-
if operation.get("method") != "GET":
|
|
81
|
-
return False
|
|
82
|
-
parsed = urlsplit(operation.get("path", ""))
|
|
83
|
-
query = parse_qsl(parsed.query)
|
|
84
|
-
mine_requested = any(key == "mine" and value == "1" for key, value in query)
|
|
85
|
-
return parsed.path == PUBLIC_AGENT_DIRECTORY_PATH and not mine_requested
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
from .portable_store import RECORD_NAMES, _validate_record
|
|
8
|
+
|
|
9
|
+
SERVICE_NAME = "TokensMind Agent Network"
|
|
10
|
+
APPLICATION_NAME = "tokensmind-agent-network"
|
|
11
|
+
MACOS_SECURITY_PATH = "/usr/bin/security"
|
|
12
|
+
MACOS_ITEM_NOT_FOUND = 44
|
|
13
|
+
SYSTEM_STORE_PROBE_LENGTH = 1024
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SystemCredentialStoreError(RuntimeError):
|
|
17
|
+
code = "SYSTEM_CREDENTIAL_STORE_UNAVAILABLE"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _validate_record_name(name):
|
|
21
|
+
if name not in RECORD_NAMES:
|
|
22
|
+
raise ValueError("Unsupported portable state record: %s" % name)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _parse_record(name, source):
|
|
26
|
+
try:
|
|
27
|
+
value = json.loads(source)
|
|
28
|
+
except json.JSONDecodeError as error:
|
|
29
|
+
raise SystemCredentialStoreError(
|
|
30
|
+
"System Agent Network %s credential is invalid JSON" % name,
|
|
31
|
+
) from error
|
|
32
|
+
try:
|
|
33
|
+
_validate_record(name, value)
|
|
34
|
+
except ValueError as error:
|
|
35
|
+
raise SystemCredentialStoreError(
|
|
36
|
+
"System Agent Network %s credential has an invalid structure" % name,
|
|
37
|
+
) from error
|
|
38
|
+
return value
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _command_failure(backend, operation, result):
|
|
42
|
+
return SystemCredentialStoreError(
|
|
43
|
+
"%s %s failed with exit code %s" % (backend, operation, result.returncode),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def run_credential_command(args, input_text=""):
|
|
48
|
+
return subprocess.run(
|
|
49
|
+
args,
|
|
50
|
+
input=input_text,
|
|
51
|
+
text=True,
|
|
52
|
+
stdout=subprocess.PIPE,
|
|
53
|
+
stderr=subprocess.PIPE,
|
|
54
|
+
check=False,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _run(runner, args, input_text, backend, operation):
|
|
59
|
+
try:
|
|
60
|
+
return runner(args, input_text)
|
|
61
|
+
except OSError as error:
|
|
62
|
+
raise SystemCredentialStoreError(
|
|
63
|
+
"%s %s failed" % (backend, operation),
|
|
64
|
+
) from error
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _verify_round_trip(write, read, remove, expected):
|
|
68
|
+
failure = None
|
|
69
|
+
try:
|
|
70
|
+
write(expected)
|
|
71
|
+
if read() != expected:
|
|
72
|
+
failure = SystemCredentialStoreError(
|
|
73
|
+
"System credential store round-trip failed",
|
|
74
|
+
)
|
|
75
|
+
except SystemCredentialStoreError as error:
|
|
76
|
+
failure = error
|
|
77
|
+
try:
|
|
78
|
+
remove()
|
|
79
|
+
except SystemCredentialStoreError as error:
|
|
80
|
+
if failure is None:
|
|
81
|
+
failure = error
|
|
82
|
+
if failure is not None:
|
|
83
|
+
raise failure
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _probe_value():
|
|
87
|
+
return str(uuid.uuid4()).ljust(SYSTEM_STORE_PROBE_LENGTH, "x")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class MacOSCredentialStore:
|
|
91
|
+
def __init__(self, namespace, runner=run_credential_command):
|
|
92
|
+
self._namespace = namespace
|
|
93
|
+
self._runner = runner
|
|
94
|
+
|
|
95
|
+
def _account(self, name):
|
|
96
|
+
_validate_record_name(name)
|
|
97
|
+
return "%s:%s" % (self._namespace, name)
|
|
98
|
+
|
|
99
|
+
def _read_source(self, account):
|
|
100
|
+
result = _run(self._runner, [
|
|
101
|
+
MACOS_SECURITY_PATH, "find-generic-password",
|
|
102
|
+
"-a", account, "-s", SERVICE_NAME, "-w",
|
|
103
|
+
], "", "macOS Keychain", "read")
|
|
104
|
+
if result.returncode == MACOS_ITEM_NOT_FOUND:
|
|
105
|
+
return None
|
|
106
|
+
if result.returncode != 0:
|
|
107
|
+
raise _command_failure("macOS Keychain", "read", result)
|
|
108
|
+
return result.stdout.strip()
|
|
109
|
+
|
|
110
|
+
def _write_source(self, account, source):
|
|
111
|
+
result = _run(self._runner, [
|
|
112
|
+
MACOS_SECURITY_PATH, "add-generic-password",
|
|
113
|
+
"-a", account, "-s", SERVICE_NAME, "-U", "-w",
|
|
114
|
+
], "%s\n%s\n" % (source, source), "macOS Keychain", "write")
|
|
115
|
+
if result.returncode != 0:
|
|
116
|
+
raise _command_failure("macOS Keychain", "write", result)
|
|
117
|
+
|
|
118
|
+
def _remove_account(self, account):
|
|
119
|
+
result = _run(self._runner, [
|
|
120
|
+
MACOS_SECURITY_PATH, "delete-generic-password",
|
|
121
|
+
"-a", account, "-s", SERVICE_NAME,
|
|
122
|
+
], "", "macOS Keychain", "remove")
|
|
123
|
+
if result.returncode not in (0, MACOS_ITEM_NOT_FOUND):
|
|
124
|
+
raise _command_failure("macOS Keychain", "remove", result)
|
|
125
|
+
|
|
126
|
+
def read(self, name):
|
|
127
|
+
source = self._read_source(self._account(name))
|
|
128
|
+
return None if source is None else _parse_record(name, source)
|
|
129
|
+
|
|
130
|
+
def write(self, name, value):
|
|
131
|
+
_validate_record(name, value)
|
|
132
|
+
self._write_source(
|
|
133
|
+
self._account(name), json.dumps(value, separators=(",", ":")),
|
|
134
|
+
)
|
|
135
|
+
if self.read(name) != value:
|
|
136
|
+
raise SystemCredentialStoreError(
|
|
137
|
+
"macOS Keychain %s write verification failed" % name,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def remove(self, name):
|
|
141
|
+
self._remove_account(self._account(name))
|
|
142
|
+
|
|
143
|
+
def verify_availability(self):
|
|
144
|
+
account = "%s:probe:%s" % (self._namespace, uuid.uuid4())
|
|
145
|
+
expected = _probe_value()
|
|
146
|
+
_verify_round_trip(
|
|
147
|
+
lambda source: self._write_source(account, source),
|
|
148
|
+
lambda: self._read_source(account),
|
|
149
|
+
lambda: self._remove_account(account),
|
|
150
|
+
expected,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
backend = "macos-keychain"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class LinuxSecretServiceStore:
|
|
157
|
+
def __init__(self, namespace, command, runner=run_credential_command):
|
|
158
|
+
self._namespace = namespace
|
|
159
|
+
self._command = command
|
|
160
|
+
self._runner = runner
|
|
161
|
+
|
|
162
|
+
def _attributes(self, name):
|
|
163
|
+
_validate_record_name(name)
|
|
164
|
+
return [
|
|
165
|
+
"application", APPLICATION_NAME,
|
|
166
|
+
"origin", self._namespace,
|
|
167
|
+
"record", name,
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
def _lookup_source(self, attributes):
|
|
171
|
+
result = _run(
|
|
172
|
+
self._runner, [self._command, "lookup"] + attributes,
|
|
173
|
+
"", "Linux Secret Service", "read",
|
|
174
|
+
)
|
|
175
|
+
missing = (
|
|
176
|
+
result.returncode == 1
|
|
177
|
+
and not result.stdout.strip()
|
|
178
|
+
and not result.stderr.strip()
|
|
179
|
+
)
|
|
180
|
+
if missing:
|
|
181
|
+
return None
|
|
182
|
+
if result.returncode != 0:
|
|
183
|
+
raise _command_failure("Linux Secret Service", "read", result)
|
|
184
|
+
return result.stdout.strip()
|
|
185
|
+
|
|
186
|
+
def _write_source(self, attributes, source):
|
|
187
|
+
result = _run(self._runner, [
|
|
188
|
+
self._command, "store", "--label=%s" % SERVICE_NAME,
|
|
189
|
+
] + attributes, source + "\n", "Linux Secret Service", "write")
|
|
190
|
+
if result.returncode != 0:
|
|
191
|
+
raise _command_failure("Linux Secret Service", "write", result)
|
|
192
|
+
|
|
193
|
+
def _remove_attributes(self, attributes):
|
|
194
|
+
if self._lookup_source(attributes) is None:
|
|
195
|
+
return
|
|
196
|
+
result = _run(
|
|
197
|
+
self._runner, [self._command, "clear"] + attributes,
|
|
198
|
+
"", "Linux Secret Service", "remove",
|
|
199
|
+
)
|
|
200
|
+
if result.returncode != 0:
|
|
201
|
+
raise _command_failure("Linux Secret Service", "remove", result)
|
|
202
|
+
|
|
203
|
+
def read(self, name):
|
|
204
|
+
source = self._lookup_source(self._attributes(name))
|
|
205
|
+
return None if source is None else _parse_record(name, source)
|
|
206
|
+
|
|
207
|
+
def write(self, name, value):
|
|
208
|
+
_validate_record(name, value)
|
|
209
|
+
self._write_source(
|
|
210
|
+
self._attributes(name), json.dumps(value, separators=(",", ":")),
|
|
211
|
+
)
|
|
212
|
+
if self.read(name) != value:
|
|
213
|
+
raise SystemCredentialStoreError(
|
|
214
|
+
"Linux Secret Service %s write verification failed" % name,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def remove(self, name):
|
|
218
|
+
self._remove_attributes(self._attributes(name))
|
|
219
|
+
|
|
220
|
+
def verify_availability(self):
|
|
221
|
+
attributes = [
|
|
222
|
+
"application", APPLICATION_NAME,
|
|
223
|
+
"origin", self._namespace,
|
|
224
|
+
"record", "probe-%s" % uuid.uuid4(),
|
|
225
|
+
]
|
|
226
|
+
expected = _probe_value()
|
|
227
|
+
_verify_round_trip(
|
|
228
|
+
lambda source: self._write_source(attributes, source),
|
|
229
|
+
lambda: self._lookup_source(attributes),
|
|
230
|
+
lambda: self._remove_attributes(attributes),
|
|
231
|
+
expected,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
backend = "linux-secret-service"
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def resolve_system_credential_store(
|
|
238
|
+
platform, namespace, env=None, *, runner=run_credential_command):
|
|
239
|
+
if platform == "darwin":
|
|
240
|
+
if not os.path.isfile(MACOS_SECURITY_PATH):
|
|
241
|
+
return None
|
|
242
|
+
if not os.access(MACOS_SECURITY_PATH, os.X_OK):
|
|
243
|
+
return None
|
|
244
|
+
return MacOSCredentialStore(namespace, runner)
|
|
245
|
+
if platform == "linux":
|
|
246
|
+
environment = os.environ if env is None else env
|
|
247
|
+
command = shutil.which("secret-tool", path=environment.get("PATH"))
|
|
248
|
+
if command:
|
|
249
|
+
return LinuxSecretServiceStore(namespace, command, runner)
|
|
250
|
+
return None
|
package/src/action-executor.js
CHANGED
|
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { ActionState, serializeFailure } from './action-errors.js';
|
|
3
3
|
import { createActionContext } from './action-context.js';
|
|
4
4
|
import { validateActionRequest } from './action-validation.js';
|
|
5
|
-
import { ensureAgent, getMyAgent, searchAgents } from './agent-actions.js';
|
|
5
|
+
import { ensureAgent, getMyAgent, searchAgents, updateAgent } from './agent-actions.js';
|
|
6
6
|
import { contactAgent } from './contact-action.js';
|
|
7
7
|
import { appeal, blockAgent, report, unblockAgent } from './governance-actions.js';
|
|
8
8
|
import {
|
|
@@ -12,12 +12,19 @@ import {
|
|
|
12
12
|
reply,
|
|
13
13
|
withdrawMessage,
|
|
14
14
|
} from './messaging-actions.js';
|
|
15
|
+
import {
|
|
16
|
+
deleteMemory,
|
|
17
|
+
getMemorySettings,
|
|
18
|
+
listMemories,
|
|
19
|
+
proposeMemory,
|
|
20
|
+
} from './memory-actions.js';
|
|
15
21
|
|
|
16
22
|
const ACTIONS = Object.freeze({
|
|
17
23
|
appeal,
|
|
18
24
|
block_agent: blockAgent,
|
|
19
25
|
contact_agent: contactAgent,
|
|
20
26
|
ensure_agent: ensureAgent,
|
|
27
|
+
update_agent: updateAgent,
|
|
21
28
|
get_conversation: getConversation,
|
|
22
29
|
get_my_agent: getMyAgent,
|
|
23
30
|
list_inbox: listInbox,
|
|
@@ -27,6 +34,10 @@ const ACTIONS = Object.freeze({
|
|
|
27
34
|
search_agents: searchAgents,
|
|
28
35
|
unblock_agent: unblockAgent,
|
|
29
36
|
withdraw_message: withdrawMessage,
|
|
37
|
+
delete_memory: deleteMemory,
|
|
38
|
+
get_memory_settings: getMemorySettings,
|
|
39
|
+
list_memories: listMemories,
|
|
40
|
+
propose_memory: proposeMemory,
|
|
30
41
|
});
|
|
31
42
|
|
|
32
43
|
function canonicalize(value) {
|
package/src/action-validation.js
CHANGED
|
@@ -5,6 +5,7 @@ export const OPERATIONS = Object.freeze([
|
|
|
5
5
|
'search_agents',
|
|
6
6
|
'get_my_agent',
|
|
7
7
|
'ensure_agent',
|
|
8
|
+
'update_agent',
|
|
8
9
|
'contact_agent',
|
|
9
10
|
'list_inbox',
|
|
10
11
|
'get_conversation',
|
|
@@ -15,6 +16,10 @@ export const OPERATIONS = Object.freeze([
|
|
|
15
16
|
'unblock_agent',
|
|
16
17
|
'report',
|
|
17
18
|
'appeal',
|
|
19
|
+
'get_memory_settings',
|
|
20
|
+
'propose_memory',
|
|
21
|
+
'list_memories',
|
|
22
|
+
'delete_memory',
|
|
18
23
|
]);
|
|
19
24
|
|
|
20
25
|
const OPERATION_SET = new Set(OPERATIONS);
|