@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
|
@@ -12,7 +12,7 @@ const CLIENT_VERSION = '0.1.0';
|
|
|
12
12
|
|
|
13
13
|
export class AuthorizationRequiredError extends Error {
|
|
14
14
|
constructor(verificationUrl) {
|
|
15
|
-
super('
|
|
15
|
+
super('Default browser handoff failed; resume the pending action to continue polling.');
|
|
16
16
|
this.name = 'AuthorizationRequiredError';
|
|
17
17
|
this.status = 'authorization_required';
|
|
18
18
|
this.verificationUrl = verificationUrl;
|
|
@@ -54,7 +54,19 @@ export function createActionApi(options = {}) {
|
|
|
54
54
|
baseUrl, stateDir, platform, env, homeDir, hostname, browser, http, store, writeEvent,
|
|
55
55
|
} = resolveOptions(options);
|
|
56
56
|
const origin = normalizeBaseUrl(baseUrl);
|
|
57
|
-
const resolvedStore = store || createCredentialStore({
|
|
57
|
+
const resolvedStore = store || createCredentialStore({
|
|
58
|
+
stateDir,
|
|
59
|
+
origin,
|
|
60
|
+
platform,
|
|
61
|
+
env,
|
|
62
|
+
homeDir,
|
|
63
|
+
onFallback: ({ backend, directory, reason }) => writeEvent({
|
|
64
|
+
event: 'credential_store_fallback',
|
|
65
|
+
backend,
|
|
66
|
+
directory,
|
|
67
|
+
reason,
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
58
70
|
const workflowStore = createWorkflowStore({ stateDir, origin, platform, homeDir });
|
|
59
71
|
const connector = createConnector({
|
|
60
72
|
baseUrl: origin,
|
|
@@ -19,7 +19,7 @@ async function resolveTarget(context) {
|
|
|
19
19
|
const name = text(target.name);
|
|
20
20
|
if (!name) requireInput(['target.id or target.name']);
|
|
21
21
|
const agents = await context.get(
|
|
22
|
-
`/agent-network-api/agents?q=${encodeURIComponent(name)}
|
|
22
|
+
`/agent-network-api/agents?q=${encodeURIComponent(name)}`,
|
|
23
23
|
);
|
|
24
24
|
if (!Array.isArray(agents) || agents.length === 0) {
|
|
25
25
|
actionFailure('TARGET_AGENT_NOT_FOUND', `No Agent matched "${name}".`);
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { actionFailure, requireInput } from './action-errors.js';
|
|
2
|
+
import { getMyAgent } from './agent-actions.js';
|
|
3
|
+
import { isObject, optionalLimit, requiredId, requiredText, text } from './action-validation.js';
|
|
4
|
+
|
|
5
|
+
const MEMORY_KINDS = new Set(['agent_experience', 'matching_preference']);
|
|
6
|
+
const MEMORY_POLARITIES = new Set(['negative', 'positive']);
|
|
7
|
+
const MEMORY_SCOPES = new Set(['contextual', 'global']);
|
|
8
|
+
// platform_event 由服务端独占写入,客户端提交会被拒绝。
|
|
9
|
+
const MEMORY_SOURCES = new Set(['model_inferred', 'user_explicit']);
|
|
10
|
+
const MEMORY_STATUSES = new Set(['active', 'deleted', 'pending_review', 'rejected']);
|
|
11
|
+
const MAX_INFERRED_CONFIDENCE = 0.6;
|
|
12
|
+
|
|
13
|
+
function requireChoice(input, field, allowed) {
|
|
14
|
+
const value = text(input?.[field]).toLowerCase();
|
|
15
|
+
if (!allowed.has(value)) requireInput([field], `${field} has an unsupported value.`);
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function optionalConfidence(value, sourceType) {
|
|
20
|
+
if (value === undefined || value === null || value === '') {
|
|
21
|
+
return sourceType === 'model_inferred' ? MAX_INFERRED_CONFIDENCE : 1;
|
|
22
|
+
}
|
|
23
|
+
const confidence = Number(value);
|
|
24
|
+
const inferredLimitExceeded = sourceType === 'model_inferred'
|
|
25
|
+
&& confidence > MAX_INFERRED_CONFIDENCE;
|
|
26
|
+
if (!Number.isFinite(confidence) || confidence <= 0 || confidence > 1 || inferredLimitExceeded) {
|
|
27
|
+
requireInput(['confidence'], 'confidence must be above 0 and no more than 0.6 for inferred memory.');
|
|
28
|
+
}
|
|
29
|
+
return confidence;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function ownedAgent(context) {
|
|
33
|
+
const agent = await getMyAgent(context);
|
|
34
|
+
if (!agent) actionFailure('AGENT_PROFILE_REQUIRED', 'Create the account Agent profile first.');
|
|
35
|
+
return agent;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function agentPath(agentId, suffix) {
|
|
39
|
+
return `/agent-network-api/agents/${encodeURIComponent(agentId)}/${suffix}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function optionalText(input, field) {
|
|
43
|
+
return Object.hasOwn(input, field) ? text(input[field]) || null : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function memoryBody(input) {
|
|
47
|
+
if (!isObject(input)) requireInput(['input']);
|
|
48
|
+
const sourceType = requireChoice(input, 'sourceType', MEMORY_SOURCES);
|
|
49
|
+
const scope = input.scope ? requireChoice(input, 'scope', MEMORY_SCOPES) : 'contextual';
|
|
50
|
+
const contextText = scope === 'global' ? '' : requiredText(input, 'contextText');
|
|
51
|
+
return {
|
|
52
|
+
clientMemoryId: requiredText(input, 'clientMemoryId'),
|
|
53
|
+
confidence: optionalConfidence(input.confidence, sourceType),
|
|
54
|
+
contextText,
|
|
55
|
+
conversationId: optionalText(input, 'conversationId'),
|
|
56
|
+
expiresAt: optionalText(input, 'expiresAt'),
|
|
57
|
+
kind: requireChoice(input, 'kind', MEMORY_KINDS),
|
|
58
|
+
polarity: requireChoice(input, 'polarity', MEMORY_POLARITIES),
|
|
59
|
+
requirementId: optionalText(input, 'requirementId'),
|
|
60
|
+
scope,
|
|
61
|
+
sourceType,
|
|
62
|
+
statement: requiredText(input, 'statement'),
|
|
63
|
+
targetAgentId: optionalText(input, 'targetAgentId'),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function listQuery(input) {
|
|
68
|
+
const params = new URLSearchParams({ limit: String(optionalLimit(input.limit)) });
|
|
69
|
+
if (input.offset !== undefined) {
|
|
70
|
+
const offset = Number(input.offset);
|
|
71
|
+
if (!Number.isInteger(offset) || offset < 0) requireInput(['offset']);
|
|
72
|
+
params.set('offset', String(offset));
|
|
73
|
+
}
|
|
74
|
+
if (input.status) params.set('status', requireChoice(input, 'status', MEMORY_STATUSES));
|
|
75
|
+
return params.toString();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function getMemorySettings(context) {
|
|
79
|
+
const agent = await ownedAgent(context);
|
|
80
|
+
return context.get(agentPath(agent.id, 'memory-settings'));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function proposeMemory(context) {
|
|
84
|
+
const body = memoryBody(context.input);
|
|
85
|
+
const agent = await ownedAgent(context);
|
|
86
|
+
const settings = await context.get(agentPath(agent.id, 'memory-settings'));
|
|
87
|
+
if (settings?.collectionEnabled !== true) {
|
|
88
|
+
actionFailure('MEMORY_COLLECTION_DISABLED', 'Memory collection is disabled for this Agent.');
|
|
89
|
+
}
|
|
90
|
+
return context.mutate({
|
|
91
|
+
method: 'POST',
|
|
92
|
+
path: agentPath(agent.id, 'memories'),
|
|
93
|
+
body,
|
|
94
|
+
label: 'memory:propose',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function listMemories(context) {
|
|
99
|
+
const query = listQuery(context.input);
|
|
100
|
+
const agent = await ownedAgent(context);
|
|
101
|
+
return context.get(`${agentPath(agent.id, 'memories')}?${query}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function deleteMemory(context) {
|
|
105
|
+
const memoryId = requiredId(context.input, 'memoryId');
|
|
106
|
+
const agent = await ownedAgent(context);
|
|
107
|
+
return context.mutate({
|
|
108
|
+
method: 'DELETE',
|
|
109
|
+
path: `${agentPath(agent.id, 'memories')}/${encodeURIComponent(memoryId)}`,
|
|
110
|
+
label: 'memory:delete',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
} from './deviceAuthorization.js';
|
|
9
9
|
import {
|
|
10
10
|
buildBusinessUrl,
|
|
11
|
-
isPublicDiscoveryRequest,
|
|
12
11
|
validateBusinessRequest,
|
|
13
12
|
} from './requestPolicy.js';
|
|
14
13
|
|
|
@@ -47,15 +46,6 @@ function assertActiveCredential(active) {
|
|
|
47
46
|
return active;
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
function authorizationPending(verificationUrl) {
|
|
51
|
-
const error = new Error(
|
|
52
|
-
'Complete Agent Network authorization in the browser, then retry the action.',
|
|
53
|
-
);
|
|
54
|
-
error.status = 'authorization_required';
|
|
55
|
-
error.verificationUrl = verificationUrl;
|
|
56
|
-
return error;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
49
|
async function executeBusinessRequest({ operation, token, baseUrl, http, signal }) {
|
|
60
50
|
return http.request({
|
|
61
51
|
url: buildBusinessUrl(baseUrl, operation.path),
|
|
@@ -90,7 +80,6 @@ async function authorize({ operation, store, browser, baseUrl, http, client, sig
|
|
|
90
80
|
pending = await createRemoteAuthorization({ pending, baseUrl, http, client, signal });
|
|
91
81
|
await store.write('pending', pending);
|
|
92
82
|
await browser.open(pending.authorization.verificationUrl);
|
|
93
|
-
throw authorizationPending(pending.authorization.verificationUrl);
|
|
94
83
|
}
|
|
95
84
|
if (pending.phase === 'authorizing') {
|
|
96
85
|
await waitForApproval({ pending, baseUrl, http, signal });
|
|
@@ -131,6 +120,11 @@ function isInvalidAgentCredential(error) {
|
|
|
131
120
|
].includes(error.code);
|
|
132
121
|
}
|
|
133
122
|
|
|
123
|
+
function requiresAuthorization(error) {
|
|
124
|
+
return error instanceof AgentNetworkHttpError
|
|
125
|
+
&& (error.status === 401 || isInvalidAgentCredential(error));
|
|
126
|
+
}
|
|
127
|
+
|
|
134
128
|
async function executeAndFinalize({ pending, active, dependencies }) {
|
|
135
129
|
assertActiveCredential(active);
|
|
136
130
|
try {
|
|
@@ -144,7 +138,7 @@ async function executeAndFinalize({ pending, active, dependencies }) {
|
|
|
144
138
|
} catch (error) {
|
|
145
139
|
if (error instanceof AgentNetworkHttpError && error.status < 500 && error.status !== 429) {
|
|
146
140
|
await dependencies.store.remove('pending');
|
|
147
|
-
if (
|
|
141
|
+
if (requiresAuthorization(error)) await dependencies.store.remove('active');
|
|
148
142
|
}
|
|
149
143
|
throw error;
|
|
150
144
|
}
|
|
@@ -170,7 +164,7 @@ export function createConnector({ store, browser, baseUrl, http, client }) {
|
|
|
170
164
|
try {
|
|
171
165
|
return await executeBusinessRequest({ operation, token: active.token, baseUrl, http, signal });
|
|
172
166
|
} catch (error) {
|
|
173
|
-
if (!
|
|
167
|
+
if (!requiresAuthorization(error)) throw error;
|
|
174
168
|
await store.remove('active');
|
|
175
169
|
return authorizeAndExecute(operation, signal);
|
|
176
170
|
}
|
|
@@ -181,15 +175,10 @@ export function createConnector({ store, browser, baseUrl, http, client }) {
|
|
|
181
175
|
const normalized = validateBusinessRequest(params);
|
|
182
176
|
const operation = { ...normalized, body: params.body };
|
|
183
177
|
const reauthorize = params.reauthorize === true;
|
|
178
|
+
|
|
184
179
|
const pending = await store.read('pending');
|
|
185
180
|
const active = await store.read('active');
|
|
186
|
-
|
|
187
181
|
if (pending) return resumePending(pending, operation, signal);
|
|
188
|
-
|
|
189
|
-
if (isPublicDiscoveryRequest(operation) && !reauthorize) {
|
|
190
|
-
return executeBusinessRequest({ operation, baseUrl, http, signal });
|
|
191
|
-
}
|
|
192
|
-
|
|
193
182
|
if (!active || reauthorize) return authorizeAndExecute(operation, signal);
|
|
194
183
|
return executeWithActive(operation, active, signal);
|
|
195
184
|
}
|
|
@@ -6,7 +6,10 @@ import {
|
|
|
6
6
|
originNamespace,
|
|
7
7
|
resolvePortableStateDir,
|
|
8
8
|
} from './portableStore.js';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
resolveSystemCredentialStore,
|
|
11
|
+
SystemCredentialStoreError,
|
|
12
|
+
} from './systemCredentialStore.js';
|
|
10
13
|
|
|
11
14
|
const RECORD_NAMES = Object.freeze(['instance', 'pending', 'active']);
|
|
12
15
|
|
|
@@ -122,6 +125,53 @@ function createFileStores({ directories, origin, platform, env, homeDir, fs, ran
|
|
|
122
125
|
}));
|
|
123
126
|
}
|
|
124
127
|
|
|
128
|
+
async function validateSystemRecords(systemStore) {
|
|
129
|
+
for (const name of RECORD_NAMES) await systemStore.read(name);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function initializeStore({
|
|
133
|
+
systemStore, fallback, fallbackDirectory, legacyDirectories, storeOptions, onFallback,
|
|
134
|
+
}) {
|
|
135
|
+
const recordDirectory = path.join(
|
|
136
|
+
fallbackDirectory, originNamespace(storeOptions.origin),
|
|
137
|
+
);
|
|
138
|
+
const legacySources = createFileStores({
|
|
139
|
+
...storeOptions, directories: legacyDirectories,
|
|
140
|
+
});
|
|
141
|
+
if (!systemStore) {
|
|
142
|
+
onFallback({
|
|
143
|
+
backend: null,
|
|
144
|
+
directory: recordDirectory,
|
|
145
|
+
reason: 'No supported system credential store is available',
|
|
146
|
+
});
|
|
147
|
+
await migrateCredentialStores({ target: fallback, sources: legacySources });
|
|
148
|
+
return fallback;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
if (typeof systemStore.verifyAvailability !== 'function') {
|
|
152
|
+
throw new SystemCredentialStoreError(
|
|
153
|
+
'System credential store does not support availability verification',
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
await systemStore.verifyAvailability();
|
|
157
|
+
await validateSystemRecords(systemStore);
|
|
158
|
+
const sources = createFileStores({
|
|
159
|
+
...storeOptions, directories: [fallbackDirectory, ...legacyDirectories],
|
|
160
|
+
});
|
|
161
|
+
await migrateCredentialStores({ target: systemStore, sources });
|
|
162
|
+
return systemStore;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (!(error instanceof SystemCredentialStoreError)) throw error;
|
|
165
|
+
onFallback({
|
|
166
|
+
backend: systemStore.backend,
|
|
167
|
+
directory: recordDirectory,
|
|
168
|
+
reason: error.message,
|
|
169
|
+
});
|
|
170
|
+
await migrateCredentialStores({ target: fallback, sources: legacySources });
|
|
171
|
+
return fallback;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
125
175
|
export function createCredentialStore({
|
|
126
176
|
stateDir,
|
|
127
177
|
origin,
|
|
@@ -133,6 +183,7 @@ export function createCredentialStore({
|
|
|
133
183
|
secureStore,
|
|
134
184
|
resolveSecureStore = resolveSystemCredentialStore,
|
|
135
185
|
run,
|
|
186
|
+
onFallback = () => {},
|
|
136
187
|
} = {}) {
|
|
137
188
|
if (stateDir) {
|
|
138
189
|
return createPortableStore({ stateDir, origin, platform, env, homeDir, fs, randomId });
|
|
@@ -147,16 +198,14 @@ export function createCredentialStore({
|
|
|
147
198
|
platform, namespace: originNamespace(origin), env, fs, run,
|
|
148
199
|
})
|
|
149
200
|
: secureStore;
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
201
|
+
const legacyDirectories = resolveLegacyStateDirs({ platform, env, homeDir });
|
|
202
|
+
return initializeStore({
|
|
203
|
+
systemStore,
|
|
204
|
+
fallback,
|
|
205
|
+
fallbackDirectory,
|
|
206
|
+
legacyDirectories: [...new Set(legacyDirectories)],
|
|
207
|
+
storeOptions: { origin, platform, env, homeDir, fs, randomId },
|
|
208
|
+
onFallback,
|
|
158
209
|
});
|
|
159
|
-
await migrateCredentialStores({ target, sources });
|
|
160
|
-
return target;
|
|
161
210
|
});
|
|
162
211
|
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { INTERNAL_PATH_PATTERNS, MUTATION_METHODS } from './constants.js';
|
|
2
2
|
|
|
3
3
|
const SUPPORTED_METHODS = new Set(['GET', 'POST', 'PATCH', 'DELETE']);
|
|
4
|
-
const PUBLIC_AGENT_DIRECTORY_PATH = '/agent-network-api/agents';
|
|
5
|
-
|
|
6
4
|
export function normalizeBaseUrl(value) {
|
|
7
5
|
const url = new URL(value);
|
|
8
6
|
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
@@ -44,12 +42,3 @@ export function buildBusinessUrl(baseUrl, path) {
|
|
|
44
42
|
}
|
|
45
43
|
return url.toString();
|
|
46
44
|
}
|
|
47
|
-
|
|
48
|
-
export function isPublicDiscoveryRequest({ method, path }) {
|
|
49
|
-
if (method !== 'GET') return false;
|
|
50
|
-
const url = new URL(path, 'https://tokensmind.invalid');
|
|
51
|
-
const mineRequested = [...url.searchParams.entries()]
|
|
52
|
-
.some(([key, value]) => key === 'mine' && value === '1');
|
|
53
|
-
return url.pathname === PUBLIC_AGENT_DIRECTORY_PATH
|
|
54
|
-
&& !mineRequested;
|
|
55
|
-
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { constants as fsConstants, promises as defaultFs } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { validatePortableRecord } from './portableStore.js';
|
|
6
|
+
import {
|
|
7
|
+
commandFailure,
|
|
8
|
+
createCommandRunner,
|
|
9
|
+
runCommand,
|
|
10
|
+
SystemCredentialStoreError,
|
|
11
|
+
verifyRoundTrip,
|
|
12
|
+
} from './systemCredentialSupport.js';
|
|
13
|
+
|
|
14
|
+
export { createCommandRunner, SystemCredentialStoreError } from './systemCredentialSupport.js';
|
|
15
|
+
|
|
16
|
+
const SERVICE_NAME = 'TokensMind Agent Network';
|
|
17
|
+
const APPLICATION_NAME = 'tokensmind-agent-network';
|
|
18
|
+
const RECORD_NAMES = new Set(['active', 'pending', 'instance']);
|
|
19
|
+
const MACOS_SECURITY_PATH = '/usr/bin/security';
|
|
20
|
+
const MACOS_ITEM_NOT_FOUND = 44;
|
|
21
|
+
const SYSTEM_STORE_PROBE_LENGTH = 1024;
|
|
22
|
+
|
|
23
|
+
function validateRecordName(name) {
|
|
24
|
+
if (!RECORD_NAMES.has(name)) throw new Error(`Unsupported portable state record: ${name}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function credentialAccount(namespace, name) {
|
|
28
|
+
validateRecordName(name);
|
|
29
|
+
return `${namespace}:${name}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseRecord(name, source) {
|
|
33
|
+
let value;
|
|
34
|
+
try {
|
|
35
|
+
value = JSON.parse(source);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
throw new SystemCredentialStoreError(
|
|
38
|
+
`System Agent Network ${name} credential is invalid JSON`,
|
|
39
|
+
{ cause: error },
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
validatePortableRecord(name, value);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
throw new SystemCredentialStoreError(
|
|
46
|
+
`System Agent Network ${name} credential has an invalid structure`,
|
|
47
|
+
{ cause: error },
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function macPasswordInput(source) {
|
|
54
|
+
return `${source}\n${source}\n`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createProbeValue() {
|
|
58
|
+
return randomUUID().padEnd(SYSTEM_STORE_PROBE_LENGTH, 'x');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createMacCommands(run) {
|
|
62
|
+
const backend = 'macOS Keychain';
|
|
63
|
+
async function readSource(account) {
|
|
64
|
+
const result = await runCommand({
|
|
65
|
+
run, backend, operation: 'read',
|
|
66
|
+
request: {
|
|
67
|
+
command: MACOS_SECURITY_PATH,
|
|
68
|
+
args: ['find-generic-password', '-a', account, '-s', SERVICE_NAME, '-w'],
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
if (result.code === MACOS_ITEM_NOT_FOUND) return null;
|
|
72
|
+
if (result.code !== 0) throw commandFailure(backend, 'read', result);
|
|
73
|
+
return result.stdout.trim();
|
|
74
|
+
}
|
|
75
|
+
async function writeSource(account, source) {
|
|
76
|
+
const result = await runCommand({
|
|
77
|
+
run, backend, operation: 'write',
|
|
78
|
+
request: {
|
|
79
|
+
command: MACOS_SECURITY_PATH,
|
|
80
|
+
args: ['add-generic-password', '-a', account, '-s', SERVICE_NAME, '-U', '-w'],
|
|
81
|
+
input: macPasswordInput(source),
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
if (result.code !== 0) throw commandFailure(backend, 'write', result);
|
|
85
|
+
}
|
|
86
|
+
async function removeAccount(account) {
|
|
87
|
+
const result = await runCommand({
|
|
88
|
+
run, backend, operation: 'remove',
|
|
89
|
+
request: {
|
|
90
|
+
command: MACOS_SECURITY_PATH,
|
|
91
|
+
args: ['delete-generic-password', '-a', account, '-s', SERVICE_NAME],
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
if (![0, MACOS_ITEM_NOT_FOUND].includes(result.code)) {
|
|
95
|
+
throw commandFailure(backend, 'remove', result);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { readSource, writeSource, removeAccount };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function createMacOperations({ namespace, run }) {
|
|
102
|
+
const { readSource, writeSource, removeAccount } = createMacCommands(run);
|
|
103
|
+
async function readRecord(name) {
|
|
104
|
+
const source = await readSource(credentialAccount(namespace, name));
|
|
105
|
+
return source === null ? null : parseRecord(name, source);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
read: readRecord,
|
|
109
|
+
async write(name, value) {
|
|
110
|
+
validateRecordName(name);
|
|
111
|
+
validatePortableRecord(name, value);
|
|
112
|
+
const account = credentialAccount(namespace, name);
|
|
113
|
+
await writeSource(account, JSON.stringify(value));
|
|
114
|
+
const stored = await readRecord(name);
|
|
115
|
+
if (!isDeepStrictEqual(stored, value)) {
|
|
116
|
+
throw new SystemCredentialStoreError(`macOS Keychain ${name} write verification failed`);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
async remove(name) {
|
|
120
|
+
await removeAccount(credentialAccount(namespace, name));
|
|
121
|
+
},
|
|
122
|
+
async verifyAvailability() {
|
|
123
|
+
const account = `${namespace}:probe:${randomUUID()}`;
|
|
124
|
+
const expected = createProbeValue();
|
|
125
|
+
await verifyRoundTrip({
|
|
126
|
+
expected,
|
|
127
|
+
write: (source) => writeSource(account, source),
|
|
128
|
+
read: () => readSource(account),
|
|
129
|
+
remove: () => removeAccount(account),
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
backend: 'macos-keychain',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function linuxAttributes(namespace, name) {
|
|
137
|
+
validateRecordName(name);
|
|
138
|
+
return ['application', APPLICATION_NAME, 'origin', namespace, 'record', name];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function createLinuxCommands({ command, run }) {
|
|
142
|
+
const backend = 'Linux Secret Service';
|
|
143
|
+
async function lookupSource(attributes) {
|
|
144
|
+
const result = await runCommand({
|
|
145
|
+
run, backend, operation: 'read',
|
|
146
|
+
request: { command, args: ['lookup', ...attributes] },
|
|
147
|
+
});
|
|
148
|
+
const missing = result.code === 1 && !result.stdout.trim() && !result.stderr.trim();
|
|
149
|
+
if (missing) return null;
|
|
150
|
+
if (result.code !== 0) throw commandFailure(backend, 'read', result);
|
|
151
|
+
return result.stdout.trim();
|
|
152
|
+
}
|
|
153
|
+
async function writeSource(attributes, source) {
|
|
154
|
+
const result = await runCommand({
|
|
155
|
+
run, backend, operation: 'write',
|
|
156
|
+
request: {
|
|
157
|
+
command,
|
|
158
|
+
args: ['store', `--label=${SERVICE_NAME}`, ...attributes],
|
|
159
|
+
input: `${source}\n`,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
if (result.code !== 0) throw commandFailure(backend, 'write', result);
|
|
163
|
+
}
|
|
164
|
+
async function removeAttributes(attributes) {
|
|
165
|
+
if (await lookupSource(attributes) === null) return;
|
|
166
|
+
const result = await runCommand({
|
|
167
|
+
run, backend, operation: 'remove',
|
|
168
|
+
request: { command, args: ['clear', ...attributes] },
|
|
169
|
+
});
|
|
170
|
+
if (result.code !== 0) throw commandFailure(backend, 'remove', result);
|
|
171
|
+
}
|
|
172
|
+
return { lookupSource, writeSource, removeAttributes };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function createLinuxOperations({ namespace, command, run }) {
|
|
176
|
+
const { lookupSource, writeSource, removeAttributes } = createLinuxCommands({ command, run });
|
|
177
|
+
async function readRecord(name) {
|
|
178
|
+
const source = await lookupSource(linuxAttributes(namespace, name));
|
|
179
|
+
return source === null ? null : parseRecord(name, source);
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
read: readRecord,
|
|
183
|
+
async write(name, value) {
|
|
184
|
+
validatePortableRecord(name, value);
|
|
185
|
+
const attributes = linuxAttributes(namespace, name);
|
|
186
|
+
await writeSource(attributes, JSON.stringify(value));
|
|
187
|
+
if (!isDeepStrictEqual(await readRecord(name), value)) {
|
|
188
|
+
throw new SystemCredentialStoreError(`Linux Secret Service ${name} write verification failed`);
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
async remove(name) {
|
|
192
|
+
await removeAttributes(linuxAttributes(namespace, name));
|
|
193
|
+
},
|
|
194
|
+
async verifyAvailability() {
|
|
195
|
+
const probeName = `probe-${randomUUID()}`;
|
|
196
|
+
const attributes = [
|
|
197
|
+
'application', APPLICATION_NAME, 'origin', namespace, 'record', probeName,
|
|
198
|
+
];
|
|
199
|
+
const expected = createProbeValue();
|
|
200
|
+
await verifyRoundTrip({
|
|
201
|
+
expected,
|
|
202
|
+
write: (source) => writeSource(attributes, source),
|
|
203
|
+
read: () => lookupSource(attributes),
|
|
204
|
+
remove: () => removeAttributes(attributes),
|
|
205
|
+
});
|
|
206
|
+
},
|
|
207
|
+
backend: 'linux-secret-service',
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function isExecutable(fs, filePath) {
|
|
212
|
+
try {
|
|
213
|
+
await fs.access(filePath, fsConstants.X_OK);
|
|
214
|
+
return true;
|
|
215
|
+
} catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function findExecutable({ name, env, fs }) {
|
|
221
|
+
const directories = String(env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
222
|
+
for (const directory of directories) {
|
|
223
|
+
const candidate = path.join(directory, name);
|
|
224
|
+
if (await isExecutable(fs, candidate)) return candidate;
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function resolveSystemCredentialStore({
|
|
230
|
+
platform,
|
|
231
|
+
namespace,
|
|
232
|
+
env = process.env,
|
|
233
|
+
fs = defaultFs,
|
|
234
|
+
run = createCommandRunner(),
|
|
235
|
+
} = {}) {
|
|
236
|
+
if (platform === 'darwin') {
|
|
237
|
+
if (!await isExecutable(fs, MACOS_SECURITY_PATH)) return null;
|
|
238
|
+
return createMacOperations({ namespace, run });
|
|
239
|
+
}
|
|
240
|
+
if (platform === 'linux') {
|
|
241
|
+
const command = await findExecutable({ name: 'secret-tool', env, fs });
|
|
242
|
+
return command ? createLinuxOperations({ namespace, command, run }) : null;
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { spawn as defaultSpawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export class SystemCredentialStoreError extends Error {
|
|
4
|
+
constructor(message, options) {
|
|
5
|
+
super(message, options);
|
|
6
|
+
this.name = 'SystemCredentialStoreError';
|
|
7
|
+
this.code = 'SYSTEM_CREDENTIAL_STORE_UNAVAILABLE';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function commandFailure(backend, operation, result) {
|
|
12
|
+
return new SystemCredentialStoreError(
|
|
13
|
+
`${backend} ${operation} failed with exit code ${String(result.code)}`,
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function runCommand({ run, backend, operation, request }) {
|
|
18
|
+
try {
|
|
19
|
+
return await run(request);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
throw new SystemCredentialStoreError(`${backend} ${operation} failed`, { cause: error });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function verifyRoundTrip({ write, read, remove, expected }) {
|
|
26
|
+
let failure;
|
|
27
|
+
try {
|
|
28
|
+
await write(expected);
|
|
29
|
+
if (await read() !== expected) {
|
|
30
|
+
failure = new SystemCredentialStoreError('System credential store round-trip failed');
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
failure = error;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
await remove();
|
|
37
|
+
} catch (error) {
|
|
38
|
+
failure ||= error;
|
|
39
|
+
}
|
|
40
|
+
if (failure) throw failure;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createCommandRunner({ spawnImpl = defaultSpawn } = {}) {
|
|
44
|
+
return ({ command, args, input = '' }) => new Promise((resolve, reject) => {
|
|
45
|
+
const child = spawnImpl(command, args, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
46
|
+
const stdout = [];
|
|
47
|
+
const stderr = [];
|
|
48
|
+
let settled = false;
|
|
49
|
+
child.stdout.on('data', (chunk) => stdout.push(chunk));
|
|
50
|
+
child.stderr.on('data', (chunk) => stderr.push(chunk));
|
|
51
|
+
child.stdin.on('error', (error) => {
|
|
52
|
+
if (error.code === 'EPIPE' || settled) return;
|
|
53
|
+
settled = true;
|
|
54
|
+
reject(error);
|
|
55
|
+
});
|
|
56
|
+
child.on('error', (error) => {
|
|
57
|
+
if (settled) return;
|
|
58
|
+
settled = true;
|
|
59
|
+
reject(error);
|
|
60
|
+
});
|
|
61
|
+
child.on('close', (code) => {
|
|
62
|
+
if (settled) return;
|
|
63
|
+
resolve({
|
|
64
|
+
code,
|
|
65
|
+
stdout: Buffer.concat(stdout).toString('utf8'),
|
|
66
|
+
stderr: Buffer.concat(stderr).toString('utf8'),
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
child.stdin.end(input);
|
|
70
|
+
});
|
|
71
|
+
}
|