@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.
Files changed (62) hide show
  1. package/README.md +81 -7
  2. package/openclaw.plugin.json +2 -2
  3. package/package.json +18 -8
  4. package/skills/tokensmind-agent-network/SKILL.md +145 -0
  5. package/skills/tokensmind-agent-network/package.json +4 -0
  6. package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/action_support.py +3 -1
  7. package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/agent_network_runtime.py +61 -9
  8. package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/action-executor.js +12 -1
  9. package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/action-validation.js +5 -0
  10. package/skills/tokensmind-agent-network/scripts/lib/agent-actions.js +91 -0
  11. package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/api-client.js +14 -2
  12. package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/contact-action.js +1 -1
  13. package/skills/tokensmind-agent-network/scripts/lib/memory-actions.js +112 -0
  14. package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/runtime/connector.js +8 -19
  15. package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/runtime/credentialStore.js +60 -11
  16. package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/runtime/requestPolicy.js +0 -11
  17. package/skills/tokensmind-agent-network/scripts/lib/runtime/systemCredentialStore.js +245 -0
  18. package/skills/tokensmind-agent-network/scripts/lib/runtime/systemCredentialSupport.js +71 -0
  19. package/skills/tokensmind-agent-network/scripts/memory_actions.py +131 -0
  20. package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/python_runtime/connector.py +8 -8
  21. package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/python_runtime/credential_store.py +56 -12
  22. package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/python_runtime/request_policy.py +1 -13
  23. package/skills/tokensmind-agent-network/scripts/python_runtime/system_credential_store.py +250 -0
  24. package/src/action-executor.js +12 -1
  25. package/src/action-validation.js +5 -0
  26. package/src/agent-actions.js +53 -6
  27. package/src/api-client.js +14 -2
  28. package/src/contact-action.js +1 -1
  29. package/src/index.js +4 -2
  30. package/src/memory-actions.js +112 -0
  31. package/src/runtime/connector.js +8 -19
  32. package/src/runtime/credentialStore.js +60 -11
  33. package/src/runtime/requestPolicy.js +0 -11
  34. package/src/runtime/systemCredentialStore.js +143 -74
  35. package/src/runtime/systemCredentialSupport.js +71 -0
  36. package/skills/tokensmind-agent-network-runtime/SKILL.md +0 -55
  37. package/skills/tokensmind-agent-network-runtime/scripts/lib/agent-actions.js +0 -44
  38. package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/systemCredentialStore.js +0 -176
  39. package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/system_credential_store.py +0 -143
  40. /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/action_executor.py +0 -0
  41. /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/action_validation.py +0 -0
  42. /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/agent-network-runtime.mjs +0 -0
  43. /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/governance_actions.py +0 -0
  44. /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/action-context.js +0 -0
  45. /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/action-errors.js +0 -0
  46. /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/governance-actions.js +0 -0
  47. /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/messaging-actions.js +0 -0
  48. /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/runtime/browser.js +0 -0
  49. /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/runtime/constants.js +0 -0
  50. /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/runtime/crypto.js +0 -0
  51. /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/runtime/deviceAuthorization.js +0 -0
  52. /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/runtime/httpClient.js +0 -0
  53. /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/runtime/portableStore.js +0 -0
  54. /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/lib/workflow-store.js +0 -0
  55. /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/python_runtime/__init__.py +0 -0
  56. /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/python_runtime/browser.py +0 -0
  57. /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/python_runtime/constants.py +0 -0
  58. /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/python_runtime/crypto.py +0 -0
  59. /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/python_runtime/device_authorization.py +0 -0
  60. /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/python_runtime/errors.py +0 -0
  61. /package/skills/{tokensmind-agent-network-runtime → tokensmind-agent-network}/scripts/python_runtime/http_client.py +0 -0
  62. /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('Complete Agent Network authorization in the browser, then retry the action.');
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({ stateDir, origin, platform, env, homeDir });
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)}&limit=20`,
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 (isInvalidAgentCredential(error)) await dependencies.store.remove('active');
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 (!isInvalidAgentCredential(error)) throw error;
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 { resolveSystemCredentialStore } from './systemCredentialStore.js';
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 target = systemStore || fallback;
151
- const sourceDirectories = [
152
- ...(systemStore ? [fallbackDirectory] : []),
153
- ...resolveLegacyStateDirs({ platform, env, homeDir }),
154
- ];
155
- const sources = createFileStores({
156
- directories: [...new Set(sourceDirectories)],
157
- origin, platform, env, homeDir, fs, randomId,
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
+ }