@tokensmind/agent-network 0.1.0

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 (63) hide show
  1. package/README.md +68 -0
  2. package/openclaw.plugin.json +32 -0
  3. package/package.json +55 -0
  4. package/skills/tokensmind-agent-network-runtime/SKILL.md +55 -0
  5. package/skills/tokensmind-agent-network-runtime/scripts/action_executor.py +112 -0
  6. package/skills/tokensmind-agent-network-runtime/scripts/action_support.py +106 -0
  7. package/skills/tokensmind-agent-network-runtime/scripts/action_validation.py +38 -0
  8. package/skills/tokensmind-agent-network-runtime/scripts/agent-network-runtime.mjs +54 -0
  9. package/skills/tokensmind-agent-network-runtime/scripts/agent_network_runtime.py +236 -0
  10. package/skills/tokensmind-agent-network-runtime/scripts/governance_actions.py +57 -0
  11. package/skills/tokensmind-agent-network-runtime/scripts/lib/action-context.js +23 -0
  12. package/skills/tokensmind-agent-network-runtime/scripts/lib/action-errors.js +55 -0
  13. package/skills/tokensmind-agent-network-runtime/scripts/lib/action-executor.js +126 -0
  14. package/skills/tokensmind-agent-network-runtime/scripts/lib/action-validation.js +61 -0
  15. package/skills/tokensmind-agent-network-runtime/scripts/lib/agent-actions.js +44 -0
  16. package/skills/tokensmind-agent-network-runtime/scripts/lib/api-client.js +76 -0
  17. package/skills/tokensmind-agent-network-runtime/scripts/lib/contact-action.js +154 -0
  18. package/skills/tokensmind-agent-network-runtime/scripts/lib/governance-actions.js +66 -0
  19. package/skills/tokensmind-agent-network-runtime/scripts/lib/messaging-actions.js +60 -0
  20. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/browser.js +26 -0
  21. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/connector.js +204 -0
  22. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/constants.js +10 -0
  23. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/credentialStore.js +162 -0
  24. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/crypto.js +25 -0
  25. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/deviceAuthorization.js +194 -0
  26. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/httpClient.js +54 -0
  27. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/portableStore.js +193 -0
  28. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/requestPolicy.js +55 -0
  29. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/systemCredentialStore.js +176 -0
  30. package/skills/tokensmind-agent-network-runtime/scripts/lib/workflow-store.js +77 -0
  31. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/__init__.py +1 -0
  32. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/browser.py +27 -0
  33. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/connector.py +156 -0
  34. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/constants.py +12 -0
  35. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/credential_store.py +135 -0
  36. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/crypto.py +28 -0
  37. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/device_authorization.py +165 -0
  38. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/errors.py +9 -0
  39. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/http_client.py +50 -0
  40. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/portable_store.py +195 -0
  41. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/request_policy.py +85 -0
  42. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/system_credential_store.py +143 -0
  43. package/src/action-context.js +23 -0
  44. package/src/action-errors.js +55 -0
  45. package/src/action-executor.js +126 -0
  46. package/src/action-validation.js +61 -0
  47. package/src/agent-actions.js +44 -0
  48. package/src/api-client.js +76 -0
  49. package/src/contact-action.js +154 -0
  50. package/src/governance-actions.js +66 -0
  51. package/src/index.js +70 -0
  52. package/src/messaging-actions.js +60 -0
  53. package/src/runtime/browser.js +26 -0
  54. package/src/runtime/connector.js +204 -0
  55. package/src/runtime/constants.js +10 -0
  56. package/src/runtime/credentialStore.js +162 -0
  57. package/src/runtime/crypto.js +25 -0
  58. package/src/runtime/deviceAuthorization.js +194 -0
  59. package/src/runtime/httpClient.js +54 -0
  60. package/src/runtime/portableStore.js +193 -0
  61. package/src/runtime/requestPolicy.js +55 -0
  62. package/src/runtime/systemCredentialStore.js +176 -0
  63. package/src/workflow-store.js +77 -0
@@ -0,0 +1,25 @@
1
+ import { createHash, randomBytes, randomUUID } from 'node:crypto';
2
+
3
+ function randomCredential(prefix) {
4
+ return `${prefix}${randomBytes(32).toString('base64url')}`;
5
+ }
6
+
7
+ export function sha256(value) {
8
+ return createHash('sha256').update(value, 'utf8').digest('hex');
9
+ }
10
+
11
+ export function createAuthorizationMaterial({ devicePrefix, tokenPrefix }) {
12
+ const deviceCode = randomCredential(devicePrefix);
13
+ const token = randomCredential(tokenPrefix);
14
+
15
+ return {
16
+ authorizationId: randomUUID(),
17
+ createIdempotencyKey: randomUUID(),
18
+ exchangeIdempotencyKey: randomUUID(),
19
+ deviceCode,
20
+ deviceCodeHash: sha256(deviceCode),
21
+ token,
22
+ tokenHash: sha256(token),
23
+ tokenPrefix: token.slice(0, 20),
24
+ };
25
+ }
@@ -0,0 +1,194 @@
1
+ import {
2
+ AGENT_TOKEN_PREFIX,
3
+ DEVICE_AUTHORIZATION_PATH,
4
+ DEVICE_CODE_PREFIX,
5
+ } from './constants.js';
6
+ import { createAuthorizationMaterial } from './crypto.js';
7
+ import { AgentNetworkHttpError } from './httpClient.js';
8
+
9
+ const DEVICE_STATUSES = new Set(['pending', 'approved', 'denied', 'expired', 'exchanged']);
10
+ const CLIENT_FIELDS = ['name', 'version', 'deviceName', 'platform'];
11
+
12
+ function headers({ bearer, idempotencyKey } = {}) {
13
+ return {
14
+ 'Content-Type': 'application/json',
15
+ ...(bearer ? { Authorization: `Bearer ${bearer}` } : {}),
16
+ ...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
17
+ };
18
+ }
19
+
20
+ function sleep(ms, signal) {
21
+ return new Promise((resolve, reject) => {
22
+ const onAbort = () => {
23
+ clearTimeout(timer);
24
+ reject(signal.reason || new Error('Agent Network authorization aborted'));
25
+ };
26
+ const timer = setTimeout(() => {
27
+ signal?.removeEventListener('abort', onAbort);
28
+ resolve();
29
+ }, ms);
30
+ signal?.addEventListener('abort', onAbort, { once: true });
31
+ });
32
+ }
33
+
34
+ function isExpectedVerificationUrl(url, pending, baseUrl) {
35
+ const expectedPath = `/console/agent-network/authorize/${encodeURIComponent(pending.authorizationId)}`;
36
+ return url.origin === baseUrl
37
+ && url.pathname === expectedPath
38
+ && !url.search
39
+ && !url.hash;
40
+ }
41
+
42
+ function hasValidExpiry(response) {
43
+ const expiresAt = Date.parse(response.expiresAt);
44
+ return Number.isFinite(expiresAt) && expiresAt > Date.now();
45
+ }
46
+
47
+ function hasValidInterval(response) {
48
+ return Number.isInteger(response.intervalSeconds) && response.intervalSeconds > 0;
49
+ }
50
+
51
+ function assertCreateResponse(response, pending, baseUrl) {
52
+ if (response?.authorizationId !== pending.authorizationId) {
53
+ throw new Error('Agent Network authorization response has a mismatched identifier');
54
+ }
55
+ const verificationUrl = new URL(response.verificationUrl);
56
+ if (!isExpectedVerificationUrl(verificationUrl, pending, baseUrl)) {
57
+ throw new Error('Agent Network returned an invalid verification URL');
58
+ }
59
+ if (!hasValidExpiry(response)) {
60
+ throw new Error('Agent Network returned an invalid authorization expiry');
61
+ }
62
+ if (!hasValidInterval(response)) {
63
+ throw new Error('Agent Network returned an invalid polling interval');
64
+ }
65
+ if (typeof response.userCode !== 'string' || !response.userCode) {
66
+ throw new Error('Agent Network authorization response is missing its user code');
67
+ }
68
+ }
69
+
70
+ function hasMatchingCredential(exchange, pending, agentId) {
71
+ return Boolean(exchange.credential?.id)
72
+ && exchange.credential.agentId === agentId
73
+ && exchange.credential.tokenPrefix === pending.tokenPrefix;
74
+ }
75
+
76
+ function exposesCredentialMaterial(exchange) {
77
+ if (Object.hasOwn(exchange, 'apiToken')) return true;
78
+ if (Object.hasOwn(exchange.credential, 'tokenHash')) return true;
79
+ return Object.hasOwn(exchange.credential, 'apiToken');
80
+ }
81
+
82
+ function assertStatusResponse(status, pending) {
83
+ if (status?.authorizationId !== pending.authorizationId
84
+ || !DEVICE_STATUSES.has(status?.status)) {
85
+ throw new Error('Agent Network returned an invalid authorization status');
86
+ }
87
+ if (Date.parse(status.expiresAt) !== Date.parse(pending.authorization.expiresAt)) {
88
+ throw new Error('Agent Network authorization expiry changed unexpectedly');
89
+ }
90
+ }
91
+
92
+ function assertExchange(exchange, pending) {
93
+ if (exchange?.status !== 'exchanged') {
94
+ throw new Error('Agent Network credential exchange did not complete');
95
+ }
96
+ if (exchange.authorizationId !== pending.authorizationId) {
97
+ throw new Error('Agent Network credential exchange has a mismatched identifier');
98
+ }
99
+ if (exchange.credential?.tokenPrefix !== pending.tokenPrefix) {
100
+ throw new Error('Agent Network credential prefix verification failed');
101
+ }
102
+ const agentId = exchange.agent?.id || null;
103
+ if (!hasMatchingCredential(exchange, pending, agentId)) {
104
+ throw new Error('Agent Network exchange response is missing credential metadata');
105
+ }
106
+ if (exposesCredentialMaterial(exchange)) {
107
+ throw new Error('Agent Network exchange response exposed forbidden credential material');
108
+ }
109
+ }
110
+
111
+ function assertClientMetadata(client) {
112
+ if (!client || CLIENT_FIELDS.some((field) => typeof client[field] !== 'string'
113
+ || !client[field].trim())) {
114
+ throw new Error('Agent Network client metadata is incomplete');
115
+ }
116
+ }
117
+
118
+ export function createPendingAuthorization({ operation, instanceId }) {
119
+ return {
120
+ phase: 'prepared',
121
+ operation,
122
+ instanceId,
123
+ ...createAuthorizationMaterial({
124
+ devicePrefix: DEVICE_CODE_PREFIX,
125
+ tokenPrefix: AGENT_TOKEN_PREFIX,
126
+ }),
127
+ };
128
+ }
129
+
130
+ export async function createRemoteAuthorization({ pending, baseUrl, http, client, signal }) {
131
+ assertClientMetadata(client);
132
+ const response = await http.request({
133
+ url: `${baseUrl}${DEVICE_AUTHORIZATION_PATH}`,
134
+ method: 'POST',
135
+ headers: headers({ idempotencyKey: pending.createIdempotencyKey }),
136
+ body: {
137
+ authorizationId: pending.authorizationId,
138
+ deviceCodeHash: pending.deviceCodeHash,
139
+ client: {
140
+ ...client,
141
+ instanceId: pending.instanceId,
142
+ },
143
+ credential: { tokenHash: pending.tokenHash, tokenPrefix: pending.tokenPrefix },
144
+ },
145
+ signal,
146
+ });
147
+ assertCreateResponse(response, pending, baseUrl);
148
+ return { ...pending, phase: 'authorizing', authorization: response };
149
+ }
150
+
151
+ async function pollOnce({ pending, baseUrl, http, signal }) {
152
+ return http.request({
153
+ url: `${baseUrl}${DEVICE_AUTHORIZATION_PATH}/${pending.authorizationId}`,
154
+ headers: headers({ bearer: pending.deviceCode }),
155
+ signal,
156
+ });
157
+ }
158
+
159
+ export async function waitForApproval({ pending, baseUrl, http, signal }) {
160
+ const intervalMs = pending.authorization.intervalSeconds * 1000;
161
+ while (Date.now() < Date.parse(pending.authorization.expiresAt)) {
162
+ const status = await pollOnce({ pending, baseUrl, http, signal });
163
+ assertStatusResponse(status, pending);
164
+ if (status.status === 'approved' || status.status === 'exchanged') return status;
165
+ if (status.status !== 'pending') {
166
+ const denied = status.status === 'denied';
167
+ throw new AgentNetworkHttpError({
168
+ status: denied ? 403 : 410,
169
+ code: denied ? 'DEVICE_AUTHORIZATION_DENIED' : 'DEVICE_AUTHORIZATION_EXPIRED',
170
+ message: `Agent Network authorization ended with status ${status.status}`,
171
+ });
172
+ }
173
+ await sleep(intervalMs, signal);
174
+ }
175
+ throw new AgentNetworkHttpError({
176
+ status: 410,
177
+ code: 'DEVICE_AUTHORIZATION_EXPIRED',
178
+ message: 'Agent Network authorization expired',
179
+ });
180
+ }
181
+
182
+ export async function exchangeAuthorization({ pending, baseUrl, http, signal }) {
183
+ const exchange = await http.request({
184
+ url: `${baseUrl}${DEVICE_AUTHORIZATION_PATH}/${pending.authorizationId}/exchange`,
185
+ method: 'POST',
186
+ headers: headers({
187
+ bearer: pending.deviceCode,
188
+ idempotencyKey: pending.exchangeIdempotencyKey,
189
+ }),
190
+ signal,
191
+ });
192
+ assertExchange(exchange, pending);
193
+ return exchange;
194
+ }
@@ -0,0 +1,54 @@
1
+ export class AgentNetworkHttpError extends Error {
2
+ constructor({ status, code, message, response, retryAfter, correction }) {
3
+ super(message || `Agent Network request failed with HTTP ${status}`);
4
+ this.name = 'AgentNetworkHttpError';
5
+ this.status = status;
6
+ this.code = code;
7
+ this.response = response;
8
+ this.retryAfter = retryAfter;
9
+ this.correction = correction;
10
+ }
11
+ }
12
+
13
+ async function parseResponse(response) {
14
+ const text = await response.text();
15
+ if (!text) return null;
16
+ try {
17
+ return JSON.parse(text);
18
+ } catch {
19
+ throw new Error(`Agent Network returned non-JSON HTTP ${response.status}`);
20
+ }
21
+ }
22
+
23
+ function unwrapSuccess(response, payload) {
24
+ if (!response.ok) return undefined;
25
+ if (payload?.success === true && Object.hasOwn(payload, 'data')) return payload.data;
26
+ throw new Error('Agent Network returned an invalid success response envelope');
27
+ }
28
+
29
+ function throwHttpError(response, payload) {
30
+ throw new AgentNetworkHttpError({
31
+ status: response.status,
32
+ code: payload?.code,
33
+ message: payload?.message,
34
+ response: payload,
35
+ retryAfter: response.headers.get('Retry-After'),
36
+ correction: payload?.correction,
37
+ });
38
+ }
39
+
40
+ export function createHttpClient({ fetchImpl = fetch } = {}) {
41
+ return {
42
+ async request({ url, method = 'GET', headers, body, signal }) {
43
+ const response = await fetchImpl(url, {
44
+ method,
45
+ headers,
46
+ body: body === undefined ? undefined : JSON.stringify(body),
47
+ signal,
48
+ });
49
+ const payload = await parseResponse(response);
50
+ if (response.ok) return unwrapSuccess(response, payload);
51
+ return throwHttpError(response, payload);
52
+ },
53
+ };
54
+ }
@@ -0,0 +1,193 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { promises as defaultFs } from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ const DIRECTORY_MODE = 0o700;
7
+ const FILE_MODE = 0o600;
8
+ const RECORD_NAMES = new Set(['active', 'pending', 'instance']);
9
+ const PENDING_PHASES = new Set(['prepared', 'authorizing', 'authorized']);
10
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
11
+ const SHA256_PATTERN = /^[a-f0-9]{64}$/;
12
+ const DEVICE_CODE_PATTERN = /^tm_device_[A-Za-z0-9_-]{43}$/;
13
+ const AGENT_TOKEN_PATTERN = /^tm_agent_[A-Za-z0-9_-]{43}$/;
14
+
15
+ function requireHomeDirectory(homeDir) {
16
+ const value = String(homeDir || '').trim();
17
+ if (!value) throw new Error('Unable to resolve the current user home directory');
18
+ return value;
19
+ }
20
+
21
+ export function resolvePortableStateDir({
22
+ platform = process.platform,
23
+ homeDir = os.homedir(),
24
+ } = {}) {
25
+ const pathApi = platform === 'win32' ? path.win32 : path;
26
+ return pathApi.join(requireHomeDirectory(homeDir), '.tokensmind', 'agent-network');
27
+ }
28
+
29
+ function isObject(value) {
30
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
31
+ }
32
+
33
+ function isNonEmptyString(value) {
34
+ return typeof value === 'string' && value.length > 0;
35
+ }
36
+
37
+ function isOperation(value) {
38
+ return isObject(value)
39
+ && ['GET', 'POST', 'PATCH', 'DELETE'].includes(value.method)
40
+ && isNonEmptyString(value.path);
41
+ }
42
+
43
+ function isInstance(value) {
44
+ return isObject(value) && UUID_PATTERN.test(value.id);
45
+ }
46
+
47
+ function isActive(value) {
48
+ return isObject(value)
49
+ && AGENT_TOKEN_PATTERN.test(value.token)
50
+ && (value.agentId === null || isNonEmptyString(value.agentId))
51
+ && isNonEmptyString(value.credentialId)
52
+ && isNonEmptyString(value.tokenPrefix)
53
+ && value.token.startsWith(value.tokenPrefix);
54
+ }
55
+
56
+ function hasPendingSecrets(value) {
57
+ return UUID_PATTERN.test(value.authorizationId)
58
+ && UUID_PATTERN.test(value.instanceId)
59
+ && UUID_PATTERN.test(value.createIdempotencyKey)
60
+ && UUID_PATTERN.test(value.exchangeIdempotencyKey)
61
+ && DEVICE_CODE_PATTERN.test(value.deviceCode)
62
+ && SHA256_PATTERN.test(value.deviceCodeHash)
63
+ && AGENT_TOKEN_PATTERN.test(value.token)
64
+ && SHA256_PATTERN.test(value.tokenHash)
65
+ && isNonEmptyString(value.tokenPrefix)
66
+ && value.token.startsWith(value.tokenPrefix);
67
+ }
68
+
69
+ function hasAuthorization(value) {
70
+ return isObject(value.authorization)
71
+ && value.authorization.authorizationId === value.authorizationId
72
+ && isNonEmptyString(value.authorization.verificationUrl)
73
+ && Number.isFinite(Date.parse(value.authorization.expiresAt))
74
+ && Number.isInteger(value.authorization.intervalSeconds)
75
+ && value.authorization.intervalSeconds > 0;
76
+ }
77
+
78
+ function isPending(value) {
79
+ if (!isObject(value) || !PENDING_PHASES.has(value.phase)) return false;
80
+ if (!isOperation(value.operation) || !hasPendingSecrets(value)) return false;
81
+ return value.phase === 'prepared' || hasAuthorization(value);
82
+ }
83
+
84
+ export function validatePortableRecord(name, value) {
85
+ const valid = name === 'active'
86
+ ? isActive(value)
87
+ : name === 'pending'
88
+ ? isPending(value)
89
+ : isInstance(value);
90
+ if (!valid) throw new Error(`Portable Agent Network ${name} state has an invalid structure`);
91
+ }
92
+
93
+ function validateRecordName(name) {
94
+ if (!RECORD_NAMES.has(name)) throw new Error(`Unsupported portable state record: ${name}`);
95
+ }
96
+
97
+ export function originNamespace(origin) {
98
+ const url = new URL(origin);
99
+ if (!['http:', 'https:'].includes(url.protocol) || url.origin !== origin) {
100
+ throw new Error('Portable Agent Network store requires a normalized HTTP origin');
101
+ }
102
+ return createHash('sha256').update(origin, 'utf8').digest('hex');
103
+ }
104
+
105
+ function isMissing(error) {
106
+ return error?.code === 'ENOENT';
107
+ }
108
+
109
+ async function ensurePrivateDirectory({ fs, directory, platform }) {
110
+ await fs.mkdir(directory, { recursive: true, mode: DIRECTORY_MODE });
111
+ if (platform !== 'win32') await fs.chmod(directory, DIRECTORY_MODE);
112
+ }
113
+
114
+ async function removeTemporaryFile(fs, filePath) {
115
+ try {
116
+ await fs.unlink(filePath);
117
+ } catch (error) {
118
+ if (!isMissing(error)) throw error;
119
+ }
120
+ }
121
+
122
+ function createReadOperation({ fs, recordPath }) {
123
+ return async function read(name) {
124
+ validateRecordName(name);
125
+ let source;
126
+ try {
127
+ source = await fs.readFile(recordPath(name), 'utf8');
128
+ } catch (error) {
129
+ if (isMissing(error)) return null;
130
+ throw error;
131
+ }
132
+ let value;
133
+ try {
134
+ value = JSON.parse(source);
135
+ } catch (error) {
136
+ throw new Error(`Portable Agent Network ${name} state is invalid JSON`, { cause: error });
137
+ }
138
+ validatePortableRecord(name, value);
139
+ return value;
140
+ };
141
+ }
142
+
143
+ function createWriteOperation({ fs, directory, platform, randomId, recordPath }) {
144
+ return async function write(name, value) {
145
+ validateRecordName(name);
146
+ validatePortableRecord(name, value);
147
+ await ensurePrivateDirectory({ fs, directory, platform });
148
+ const target = recordPath(name);
149
+ const temporary = path.join(directory, `.${name}.${process.pid}.${randomId()}.tmp`);
150
+ try {
151
+ await fs.writeFile(temporary, JSON.stringify(value), {
152
+ encoding: 'utf8', mode: FILE_MODE, flag: 'wx',
153
+ });
154
+ if (platform !== 'win32') await fs.chmod(temporary, FILE_MODE);
155
+ await fs.rename(temporary, target);
156
+ if (platform !== 'win32') await fs.chmod(target, FILE_MODE);
157
+ } catch (error) {
158
+ await removeTemporaryFile(fs, temporary);
159
+ throw error;
160
+ }
161
+ };
162
+ }
163
+
164
+ function createRemoveOperation({ fs, recordPath }) {
165
+ return async function remove(name) {
166
+ validateRecordName(name);
167
+ try {
168
+ await fs.unlink(recordPath(name));
169
+ } catch (error) {
170
+ if (!isMissing(error)) throw error;
171
+ }
172
+ };
173
+ }
174
+
175
+ export function createPortableStore({
176
+ stateDir,
177
+ origin,
178
+ platform = process.platform,
179
+ env = process.env,
180
+ homeDir = os.homedir(),
181
+ fs = defaultFs,
182
+ randomId = randomUUID,
183
+ } = {}) {
184
+ const baseDirectory = path.resolve(stateDir || resolvePortableStateDir({ platform, env, homeDir }));
185
+ const directory = path.join(baseDirectory, originNamespace(origin));
186
+ const recordPath = (name) => path.join(directory, `${name}.json`);
187
+ const context = { fs, directory, platform, randomId, recordPath };
188
+ return {
189
+ read: createReadOperation(context),
190
+ write: createWriteOperation(context),
191
+ remove: createRemoveOperation(context),
192
+ };
193
+ }
@@ -0,0 +1,55 @@
1
+ import { INTERNAL_PATH_PATTERNS, MUTATION_METHODS } from './constants.js';
2
+
3
+ const SUPPORTED_METHODS = new Set(['GET', 'POST', 'PATCH', 'DELETE']);
4
+ const PUBLIC_AGENT_DIRECTORY_PATH = '/agent-network-api/agents';
5
+
6
+ export function normalizeBaseUrl(value) {
7
+ const url = new URL(value);
8
+ if (!['http:', 'https:'].includes(url.protocol)) {
9
+ throw new Error('Agent Network baseUrl must use http or https');
10
+ }
11
+ url.pathname = '/';
12
+ url.search = '';
13
+ url.hash = '';
14
+ return url.origin;
15
+ }
16
+
17
+ export function validateBusinessRequest({ method, path, idempotencyKey, reauthorize }) {
18
+ const normalizedMethod = String(method).toUpperCase();
19
+ if (!SUPPORTED_METHODS.has(normalizedMethod)) {
20
+ throw new Error(`Unsupported Agent Network method: ${normalizedMethod}`);
21
+ }
22
+ if (!path.startsWith('/agent-network-api/')) {
23
+ throw new Error('Agent Network path must start with /agent-network-api/');
24
+ }
25
+ if (INTERNAL_PATH_PATTERNS.some((pattern) => pattern.test(path))) {
26
+ throw new Error('This endpoint is not available through the ordinary-user connector');
27
+ }
28
+ if (MUTATION_METHODS.has(normalizedMethod) && !idempotencyKey?.trim()) {
29
+ throw new Error('Mutating Agent Network requests require a stable Idempotency-Key');
30
+ }
31
+ if (reauthorize !== undefined && typeof reauthorize !== 'boolean') {
32
+ throw new Error('Agent Network reauthorize must be a boolean');
33
+ }
34
+ return { method: normalizedMethod, path, idempotencyKey: idempotencyKey?.trim() };
35
+ }
36
+
37
+ export function buildBusinessUrl(baseUrl, path) {
38
+ const url = new URL(path, `${baseUrl}/`);
39
+ if (url.origin !== baseUrl || !url.pathname.startsWith('/agent-network-api/')) {
40
+ throw new Error('Agent Network request must remain on the configured origin');
41
+ }
42
+ if (INTERNAL_PATH_PATTERNS.some((pattern) => pattern.test(url.pathname))) {
43
+ throw new Error('This endpoint is not available through the ordinary-user connector');
44
+ }
45
+ return url.toString();
46
+ }
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,176 @@
1
+ import { spawn as defaultSpawn } from 'node:child_process';
2
+ import { constants as fsConstants, promises as defaultFs } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { validatePortableRecord } from './portableStore.js';
5
+
6
+ const SERVICE_NAME = 'TokensMind Agent Network';
7
+ const APPLICATION_NAME = 'tokensmind-agent-network';
8
+ const RECORD_NAMES = new Set(['active', 'pending', 'instance']);
9
+ const MACOS_SECURITY_PATH = '/usr/bin/security';
10
+ const MACOS_ITEM_NOT_FOUND = 44;
11
+
12
+ function validateRecordName(name) {
13
+ if (!RECORD_NAMES.has(name)) throw new Error(`Unsupported portable state record: ${name}`);
14
+ }
15
+
16
+ function credentialAccount(namespace, name) {
17
+ validateRecordName(name);
18
+ return `${namespace}:${name}`;
19
+ }
20
+
21
+ function parseRecord(name, source) {
22
+ let value;
23
+ try {
24
+ value = JSON.parse(source);
25
+ } catch (error) {
26
+ throw new Error(`System Agent Network ${name} credential is invalid JSON`, { cause: error });
27
+ }
28
+ validatePortableRecord(name, value);
29
+ return value;
30
+ }
31
+
32
+ function commandFailure(backend, operation, result) {
33
+ return new Error(
34
+ `${backend} ${operation} failed with exit code ${String(result.code)}`,
35
+ );
36
+ }
37
+
38
+ export function createCommandRunner({ spawnImpl = defaultSpawn } = {}) {
39
+ return ({ command, args, input = '' }) => new Promise((resolve, reject) => {
40
+ const child = spawnImpl(command, args, { stdio: ['pipe', 'pipe', 'pipe'] });
41
+ const stdout = [];
42
+ const stderr = [];
43
+ let settled = false;
44
+ child.stdout.on('data', (chunk) => stdout.push(chunk));
45
+ child.stderr.on('data', (chunk) => stderr.push(chunk));
46
+ child.stdin.on('error', (error) => {
47
+ if (error.code === 'EPIPE' || settled) return;
48
+ settled = true;
49
+ reject(error);
50
+ });
51
+ child.on('error', (error) => {
52
+ if (settled) return;
53
+ settled = true;
54
+ reject(error);
55
+ });
56
+ child.on('close', (code) => {
57
+ if (settled) return;
58
+ resolve({
59
+ code,
60
+ stdout: Buffer.concat(stdout).toString('utf8'),
61
+ stderr: Buffer.concat(stderr).toString('utf8'),
62
+ });
63
+ });
64
+ child.stdin.end(input);
65
+ });
66
+ }
67
+
68
+ function createMacOperations({ namespace, run }) {
69
+ return {
70
+ async read(name) {
71
+ const account = credentialAccount(namespace, name);
72
+ const result = await run({
73
+ command: MACOS_SECURITY_PATH,
74
+ args: ['find-generic-password', '-a', account, '-s', SERVICE_NAME, '-w'],
75
+ });
76
+ if (result.code === MACOS_ITEM_NOT_FOUND) return null;
77
+ if (result.code !== 0) throw commandFailure('macOS Keychain', 'read', result);
78
+ return parseRecord(name, result.stdout.trim());
79
+ },
80
+ async write(name, value) {
81
+ validateRecordName(name);
82
+ validatePortableRecord(name, value);
83
+ const account = credentialAccount(namespace, name);
84
+ const result = await run({
85
+ command: MACOS_SECURITY_PATH,
86
+ args: ['add-generic-password', '-a', account, '-s', SERVICE_NAME, '-U', '-w'],
87
+ input: `${JSON.stringify(value)}\n`,
88
+ });
89
+ if (result.code !== 0) throw commandFailure('macOS Keychain', 'write', result);
90
+ },
91
+ async remove(name) {
92
+ const account = credentialAccount(namespace, name);
93
+ const result = await run({
94
+ command: MACOS_SECURITY_PATH,
95
+ args: ['delete-generic-password', '-a', account, '-s', SERVICE_NAME],
96
+ });
97
+ if (![0, MACOS_ITEM_NOT_FOUND].includes(result.code)) {
98
+ throw commandFailure('macOS Keychain', 'remove', result);
99
+ }
100
+ },
101
+ };
102
+ }
103
+
104
+ function linuxAttributes(namespace, name) {
105
+ validateRecordName(name);
106
+ return ['application', APPLICATION_NAME, 'origin', namespace, 'record', name];
107
+ }
108
+
109
+ function createLinuxOperations({ namespace, command, run }) {
110
+ async function lookup(name) {
111
+ const result = await run({
112
+ command,
113
+ args: ['lookup', ...linuxAttributes(namespace, name)],
114
+ });
115
+ const missing = result.code === 1 && !result.stdout.trim() && !result.stderr.trim();
116
+ if (missing) return null;
117
+ if (result.code !== 0) throw commandFailure('Linux Secret Service', 'read', result);
118
+ return parseRecord(name, result.stdout.trim());
119
+ }
120
+ return {
121
+ read: lookup,
122
+ async write(name, value) {
123
+ validatePortableRecord(name, value);
124
+ const result = await run({
125
+ command,
126
+ args: ['store', `--label=${SERVICE_NAME}`, ...linuxAttributes(namespace, name)],
127
+ input: `${JSON.stringify(value)}\n`,
128
+ });
129
+ if (result.code !== 0) throw commandFailure('Linux Secret Service', 'write', result);
130
+ },
131
+ async remove(name) {
132
+ if (await lookup(name) === null) return;
133
+ const result = await run({
134
+ command,
135
+ args: ['clear', ...linuxAttributes(namespace, name)],
136
+ });
137
+ if (result.code !== 0) throw commandFailure('Linux Secret Service', 'remove', result);
138
+ },
139
+ };
140
+ }
141
+
142
+ async function isExecutable(fs, filePath) {
143
+ try {
144
+ await fs.access(filePath, fsConstants.X_OK);
145
+ return true;
146
+ } catch {
147
+ return false;
148
+ }
149
+ }
150
+
151
+ async function findExecutable({ name, env, fs }) {
152
+ const directories = String(env.PATH || '').split(path.delimiter).filter(Boolean);
153
+ for (const directory of directories) {
154
+ const candidate = path.join(directory, name);
155
+ if (await isExecutable(fs, candidate)) return candidate;
156
+ }
157
+ return null;
158
+ }
159
+
160
+ export async function resolveSystemCredentialStore({
161
+ platform,
162
+ namespace,
163
+ env = process.env,
164
+ fs = defaultFs,
165
+ run = createCommandRunner(),
166
+ } = {}) {
167
+ if (platform === 'darwin') {
168
+ if (!await isExecutable(fs, MACOS_SECURITY_PATH)) return null;
169
+ return createMacOperations({ namespace, run });
170
+ }
171
+ if (platform === 'linux') {
172
+ const command = await findExecutable({ name: 'secret-tool', env, fs });
173
+ return command ? createLinuxOperations({ namespace, command, run }) : null;
174
+ }
175
+ return null;
176
+ }