@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.
- package/README.md +68 -0
- package/openclaw.plugin.json +32 -0
- package/package.json +55 -0
- package/skills/tokensmind-agent-network-runtime/SKILL.md +55 -0
- package/skills/tokensmind-agent-network-runtime/scripts/action_executor.py +112 -0
- package/skills/tokensmind-agent-network-runtime/scripts/action_support.py +106 -0
- package/skills/tokensmind-agent-network-runtime/scripts/action_validation.py +38 -0
- package/skills/tokensmind-agent-network-runtime/scripts/agent-network-runtime.mjs +54 -0
- package/skills/tokensmind-agent-network-runtime/scripts/agent_network_runtime.py +236 -0
- package/skills/tokensmind-agent-network-runtime/scripts/governance_actions.py +57 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/action-context.js +23 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/action-errors.js +55 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/action-executor.js +126 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/action-validation.js +61 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/agent-actions.js +44 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/api-client.js +76 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/contact-action.js +154 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/governance-actions.js +66 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/messaging-actions.js +60 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/browser.js +26 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/connector.js +204 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/constants.js +10 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/credentialStore.js +162 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/crypto.js +25 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/deviceAuthorization.js +194 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/httpClient.js +54 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/portableStore.js +193 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/requestPolicy.js +55 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/runtime/systemCredentialStore.js +176 -0
- package/skills/tokensmind-agent-network-runtime/scripts/lib/workflow-store.js +77 -0
- package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/__init__.py +1 -0
- package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/browser.py +27 -0
- package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/connector.py +156 -0
- package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/constants.py +12 -0
- package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/credential_store.py +135 -0
- package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/crypto.py +28 -0
- package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/device_authorization.py +165 -0
- package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/errors.py +9 -0
- package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/http_client.py +50 -0
- package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/portable_store.py +195 -0
- package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/request_policy.py +85 -0
- package/skills/tokensmind-agent-network-runtime/scripts/python_runtime/system_credential_store.py +143 -0
- package/src/action-context.js +23 -0
- package/src/action-errors.js +55 -0
- package/src/action-executor.js +126 -0
- package/src/action-validation.js +61 -0
- package/src/agent-actions.js +44 -0
- package/src/api-client.js +76 -0
- package/src/contact-action.js +154 -0
- package/src/governance-actions.js +66 -0
- package/src/index.js +70 -0
- package/src/messaging-actions.js +60 -0
- package/src/runtime/browser.js +26 -0
- package/src/runtime/connector.js +204 -0
- package/src/runtime/constants.js +10 -0
- package/src/runtime/credentialStore.js +162 -0
- package/src/runtime/crypto.js +25 -0
- package/src/runtime/deviceAuthorization.js +194 -0
- package/src/runtime/httpClient.js +54 -0
- package/src/runtime/portableStore.js +193 -0
- package/src/runtime/requestPolicy.js +55 -0
- package/src/runtime/systemCredentialStore.js +176 -0
- package/src/workflow-store.js +77 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { AgentNetworkHttpError } from './httpClient.js';
|
|
3
|
+
import {
|
|
4
|
+
createPendingAuthorization,
|
|
5
|
+
createRemoteAuthorization,
|
|
6
|
+
exchangeAuthorization,
|
|
7
|
+
waitForApproval,
|
|
8
|
+
} from './deviceAuthorization.js';
|
|
9
|
+
import {
|
|
10
|
+
buildBusinessUrl,
|
|
11
|
+
isPublicDiscoveryRequest,
|
|
12
|
+
validateBusinessRequest,
|
|
13
|
+
} from './requestPolicy.js';
|
|
14
|
+
|
|
15
|
+
function requestHeaders({ token, idempotencyKey }) {
|
|
16
|
+
return {
|
|
17
|
+
'Content-Type': 'application/json',
|
|
18
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
19
|
+
...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function canonicalize(value) {
|
|
24
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
25
|
+
if (!value || typeof value !== 'object') return value;
|
|
26
|
+
return Object.fromEntries(
|
|
27
|
+
Object.keys(value).sort().map((key) => [key, canonicalize(value[key])]),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function operationsMatch(left, right) {
|
|
32
|
+
return JSON.stringify(canonicalize(left)) === JSON.stringify(canonicalize(right));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function assertPendingMatches(pending, operation) {
|
|
36
|
+
if (!operationsMatch(pending.operation, operation)) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
'A different Agent Network operation is pending; retry the original operation first',
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function assertActiveCredential(active) {
|
|
44
|
+
if (!active?.token) {
|
|
45
|
+
throw new Error('Authorized Agent Network operation has no active stored credential');
|
|
46
|
+
}
|
|
47
|
+
return active;
|
|
48
|
+
}
|
|
49
|
+
|
|
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
|
+
async function executeBusinessRequest({ operation, token, baseUrl, http, signal }) {
|
|
60
|
+
return http.request({
|
|
61
|
+
url: buildBusinessUrl(baseUrl, operation.path),
|
|
62
|
+
method: operation.method,
|
|
63
|
+
headers: requestHeaders({ token, idempotencyKey: operation.idempotencyKey }),
|
|
64
|
+
body: operation.body,
|
|
65
|
+
signal,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function loadInstanceId(store) {
|
|
70
|
+
const existing = await store.read('instance');
|
|
71
|
+
if (existing?.id) return existing.id;
|
|
72
|
+
const id = randomUUID();
|
|
73
|
+
await store.write('instance', { id });
|
|
74
|
+
return id;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function preparePending({ operation, store }) {
|
|
78
|
+
const existing = await store.read('pending');
|
|
79
|
+
if (existing) return existing;
|
|
80
|
+
const instanceId = await loadInstanceId(store);
|
|
81
|
+
const pending = createPendingAuthorization({ operation, instanceId });
|
|
82
|
+
await store.write('pending', pending);
|
|
83
|
+
return pending;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function authorize({ operation, store, browser, baseUrl, http, client, signal }) {
|
|
87
|
+
try {
|
|
88
|
+
let pending = await preparePending({ operation, store });
|
|
89
|
+
if (pending.phase === 'prepared') {
|
|
90
|
+
pending = await createRemoteAuthorization({ pending, baseUrl, http, client, signal });
|
|
91
|
+
await store.write('pending', pending);
|
|
92
|
+
await browser.open(pending.authorization.verificationUrl);
|
|
93
|
+
throw authorizationPending(pending.authorization.verificationUrl);
|
|
94
|
+
}
|
|
95
|
+
if (pending.phase === 'authorizing') {
|
|
96
|
+
await waitForApproval({ pending, baseUrl, http, signal });
|
|
97
|
+
const exchange = await exchangeAuthorization({ pending, baseUrl, http, signal });
|
|
98
|
+
const active = {
|
|
99
|
+
token: pending.token,
|
|
100
|
+
agentId: exchange.agent?.id || null,
|
|
101
|
+
credentialId: exchange.credential.id,
|
|
102
|
+
tokenPrefix: exchange.credential.tokenPrefix,
|
|
103
|
+
};
|
|
104
|
+
await store.write('active', active);
|
|
105
|
+
pending = { ...pending, phase: 'authorized' };
|
|
106
|
+
await store.write('pending', pending);
|
|
107
|
+
return active;
|
|
108
|
+
}
|
|
109
|
+
return store.read('active');
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (isTerminalAuthorizationError(error)) await store.remove('pending');
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isTerminalAuthorizationError(error) {
|
|
117
|
+
return error instanceof AgentNetworkHttpError
|
|
118
|
+
&& [
|
|
119
|
+
'DEVICE_AUTHORIZATION_DENIED',
|
|
120
|
+
'DEVICE_AUTHORIZATION_EXCHANGED',
|
|
121
|
+
'DEVICE_AUTHORIZATION_EXPIRED',
|
|
122
|
+
].includes(error.code);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isInvalidAgentCredential(error) {
|
|
126
|
+
return error instanceof AgentNetworkHttpError
|
|
127
|
+
&& [
|
|
128
|
+
'AGENT_CREDENTIAL_EXPIRED',
|
|
129
|
+
'AGENT_CREDENTIAL_INVALID',
|
|
130
|
+
'AGENT_CREDENTIAL_REVOKED',
|
|
131
|
+
].includes(error.code);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function executeAndFinalize({ pending, active, dependencies }) {
|
|
135
|
+
assertActiveCredential(active);
|
|
136
|
+
try {
|
|
137
|
+
const result = await executeBusinessRequest({
|
|
138
|
+
operation: pending.operation,
|
|
139
|
+
token: active.token,
|
|
140
|
+
...dependencies,
|
|
141
|
+
});
|
|
142
|
+
await dependencies.store.remove('pending');
|
|
143
|
+
return result;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (error instanceof AgentNetworkHttpError && error.status < 500 && error.status !== 429) {
|
|
146
|
+
await dependencies.store.remove('pending');
|
|
147
|
+
if (isInvalidAgentCredential(error)) await dependencies.store.remove('active');
|
|
148
|
+
}
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function createConnector({ store, browser, baseUrl, http, client }) {
|
|
154
|
+
let executionQueue = Promise.resolve();
|
|
155
|
+
|
|
156
|
+
const dependencies = (signal) => ({ store, browser, baseUrl, http, client, signal });
|
|
157
|
+
|
|
158
|
+
async function authorizeAndExecute(operation, signal) {
|
|
159
|
+
const active = await authorize({ operation, ...dependencies(signal) });
|
|
160
|
+
const pending = await store.read('pending');
|
|
161
|
+
return executeAndFinalize({ pending, active, dependencies: dependencies(signal) });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function resumePending(pending, operation, signal) {
|
|
165
|
+
assertPendingMatches(pending, operation);
|
|
166
|
+
return authorizeAndExecute(pending.operation, signal);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function executeWithActive(operation, active, signal) {
|
|
170
|
+
try {
|
|
171
|
+
return await executeBusinessRequest({ operation, token: active.token, baseUrl, http, signal });
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (!isInvalidAgentCredential(error)) throw error;
|
|
174
|
+
await store.remove('active');
|
|
175
|
+
return authorizeAndExecute(operation, signal);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function executeOnce(params, { signal } = {}) {
|
|
180
|
+
if (signal?.aborted) throw signal.reason;
|
|
181
|
+
const normalized = validateBusinessRequest(params);
|
|
182
|
+
const operation = { ...normalized, body: params.body };
|
|
183
|
+
const reauthorize = params.reauthorize === true;
|
|
184
|
+
const pending = await store.read('pending');
|
|
185
|
+
const active = await store.read('active');
|
|
186
|
+
|
|
187
|
+
if (pending) return resumePending(pending, operation, signal);
|
|
188
|
+
|
|
189
|
+
if (isPublicDiscoveryRequest(operation) && !reauthorize) {
|
|
190
|
+
return executeBusinessRequest({ operation, baseUrl, http, signal });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!active || reauthorize) return authorizeAndExecute(operation, signal);
|
|
194
|
+
return executeWithActive(operation, active, signal);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
execute(params, options) {
|
|
199
|
+
const result = executionQueue.then(() => executeOnce(params, options));
|
|
200
|
+
executionQueue = result.catch(() => undefined);
|
|
201
|
+
return result;
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const DEFAULT_BASE_URL = 'https://tokensmind.ai';
|
|
2
|
+
export const DEVICE_CODE_PREFIX = 'tm_device_';
|
|
3
|
+
export const AGENT_TOKEN_PREFIX = 'tm_agent_';
|
|
4
|
+
export const DEVICE_AUTHORIZATION_PATH = '/agent-network-api/device-authorizations';
|
|
5
|
+
export const MUTATION_METHODS = new Set(['POST', 'PATCH', 'DELETE']);
|
|
6
|
+
|
|
7
|
+
export const INTERNAL_PATH_PATTERNS = [
|
|
8
|
+
/^\/agent-network-api\/device-authorizations(?:\/|$)/,
|
|
9
|
+
/^\/agent-network-api\/agents\/[^/]+\/credentials(?:\/|$)/,
|
|
10
|
+
];
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
createPortableStore,
|
|
6
|
+
originNamespace,
|
|
7
|
+
resolvePortableStateDir,
|
|
8
|
+
} from './portableStore.js';
|
|
9
|
+
import { resolveSystemCredentialStore } from './systemCredentialStore.js';
|
|
10
|
+
|
|
11
|
+
const RECORD_NAMES = Object.freeze(['instance', 'pending', 'active']);
|
|
12
|
+
|
|
13
|
+
function requireHome(homeDir) {
|
|
14
|
+
const home = String(homeDir || '').trim();
|
|
15
|
+
if (!home) throw new Error('Unable to resolve the current user home directory');
|
|
16
|
+
return home;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function windowsLegacyDirs(env) {
|
|
20
|
+
const localAppData = String(env.LOCALAPPDATA || '').trim();
|
|
21
|
+
return localAppData ? [path.win32.join(localAppData, 'TokensMind', 'AgentNetwork')] : [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function linuxLegacyDirs({ env, home }) {
|
|
25
|
+
const defaultDirectory = path.join(home, '.local', 'state', 'tokensmind', 'agent-network');
|
|
26
|
+
const xdgStateHome = String(env.XDG_STATE_HOME || '').trim();
|
|
27
|
+
if (!xdgStateHome) return [defaultDirectory];
|
|
28
|
+
return [...new Set([
|
|
29
|
+
path.join(xdgStateHome, 'tokensmind', 'agent-network'), defaultDirectory,
|
|
30
|
+
])];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveLegacyStateDirs({
|
|
34
|
+
platform = process.platform,
|
|
35
|
+
env = process.env,
|
|
36
|
+
homeDir = os.homedir(),
|
|
37
|
+
} = {}) {
|
|
38
|
+
const home = requireHome(homeDir);
|
|
39
|
+
if (platform === 'darwin') {
|
|
40
|
+
return [path.join(home, 'Library', 'Application Support', 'TokensMind', 'AgentNetwork')];
|
|
41
|
+
}
|
|
42
|
+
if (platform === 'win32') return windowsLegacyDirs(env);
|
|
43
|
+
if (platform !== 'linux') return [];
|
|
44
|
+
return linuxLegacyDirs({ env, home });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function collectSourceRecords(sources) {
|
|
48
|
+
const candidates = new Map();
|
|
49
|
+
for (const source of sources) {
|
|
50
|
+
for (const name of RECORD_NAMES) {
|
|
51
|
+
const value = await source.read(name);
|
|
52
|
+
if (value === null) continue;
|
|
53
|
+
const candidate = candidates.get(name);
|
|
54
|
+
if (candidate && !isDeepStrictEqual(candidate.value, value)) {
|
|
55
|
+
throw new Error(`Conflicting Agent Network ${name} records exist in known local stores`);
|
|
56
|
+
}
|
|
57
|
+
if (candidate) candidate.sources.push(source);
|
|
58
|
+
else candidates.set(name, { value, sources: [source] });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return candidates;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function validateMigrationTarget(target, candidates) {
|
|
65
|
+
const missing = [];
|
|
66
|
+
for (const [name, candidate] of candidates) {
|
|
67
|
+
const current = await target.read(name);
|
|
68
|
+
if (current === null) missing.push([name, candidate.value]);
|
|
69
|
+
else if (!isDeepStrictEqual(current, candidate.value)) {
|
|
70
|
+
throw new Error(`Stored Agent Network ${name} conflicts with a known legacy record`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return missing;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function verifyWrites(target, records) {
|
|
77
|
+
for (const [name, expected] of records) {
|
|
78
|
+
const actual = await target.read(name);
|
|
79
|
+
if (!isDeepStrictEqual(actual, expected)) {
|
|
80
|
+
throw new Error(`Agent Network ${name} migration could not be verified`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function removeMigratedSources(candidates) {
|
|
86
|
+
for (const [name, candidate] of candidates) {
|
|
87
|
+
for (const source of candidate.sources) await source.remove(name);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function migrateCredentialStores({ target, sources }) {
|
|
92
|
+
const candidates = await collectSourceRecords(sources);
|
|
93
|
+
if (candidates.size === 0) return;
|
|
94
|
+
const missing = await validateMigrationTarget(target, candidates);
|
|
95
|
+
for (const [name, value] of missing) await target.write(name, value);
|
|
96
|
+
await verifyWrites(target, [...candidates].map(([name, item]) => [name, item.value]));
|
|
97
|
+
await removeMigratedSources(candidates);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function createLazyStore(resolveStore) {
|
|
101
|
+
let storePromise;
|
|
102
|
+
const getStore = () => {
|
|
103
|
+
storePromise ||= resolveStore();
|
|
104
|
+
return storePromise;
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
async read(name) {
|
|
108
|
+
return (await getStore()).read(name);
|
|
109
|
+
},
|
|
110
|
+
async write(name, value) {
|
|
111
|
+
return (await getStore()).write(name, value);
|
|
112
|
+
},
|
|
113
|
+
async remove(name) {
|
|
114
|
+
return (await getStore()).remove(name);
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function createFileStores({ directories, origin, platform, env, homeDir, fs, randomId }) {
|
|
120
|
+
return directories.map((stateDir) => createPortableStore({
|
|
121
|
+
stateDir, origin, platform, env, homeDir, fs, randomId,
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function createCredentialStore({
|
|
126
|
+
stateDir,
|
|
127
|
+
origin,
|
|
128
|
+
platform = process.platform,
|
|
129
|
+
env = process.env,
|
|
130
|
+
homeDir = os.homedir(),
|
|
131
|
+
fs,
|
|
132
|
+
randomId,
|
|
133
|
+
secureStore,
|
|
134
|
+
resolveSecureStore = resolveSystemCredentialStore,
|
|
135
|
+
run,
|
|
136
|
+
} = {}) {
|
|
137
|
+
if (stateDir) {
|
|
138
|
+
return createPortableStore({ stateDir, origin, platform, env, homeDir, fs, randomId });
|
|
139
|
+
}
|
|
140
|
+
const fallbackDirectory = resolvePortableStateDir({ platform, env, homeDir });
|
|
141
|
+
const fallback = createPortableStore({
|
|
142
|
+
stateDir: fallbackDirectory, origin, platform, env, homeDir, fs, randomId,
|
|
143
|
+
});
|
|
144
|
+
return createLazyStore(async () => {
|
|
145
|
+
const systemStore = secureStore === undefined
|
|
146
|
+
? await resolveSecureStore({
|
|
147
|
+
platform, namespace: originNamespace(origin), env, fs, run,
|
|
148
|
+
})
|
|
149
|
+
: 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,
|
|
158
|
+
});
|
|
159
|
+
await migrateCredentialStores({ target, sources });
|
|
160
|
+
return target;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
@@ -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
|
+
}
|