@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,154 @@
|
|
|
1
|
+
import { actionFailure, requireInput, requireSelection } from './action-errors.js';
|
|
2
|
+
import { text } from './action-validation.js';
|
|
3
|
+
import { ensureAgent } from './agent-actions.js';
|
|
4
|
+
|
|
5
|
+
const STOPPED_REQUIREMENT_STATES = new Set(['paused', 'completed', 'cancelled']);
|
|
6
|
+
|
|
7
|
+
function exactName(agent, name) {
|
|
8
|
+
return text(agent?.name).toLocaleLowerCase() === name.toLocaleLowerCase();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function defaultTo(value, fallback) {
|
|
12
|
+
return value || fallback;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function resolveTarget(context) {
|
|
16
|
+
const target = context.input.target || {};
|
|
17
|
+
const id = text(target.id);
|
|
18
|
+
if (id) return { id, name: text(target.name) };
|
|
19
|
+
const name = text(target.name);
|
|
20
|
+
if (!name) requireInput(['target.id or target.name']);
|
|
21
|
+
const agents = await context.get(
|
|
22
|
+
`/agent-network-api/agents?q=${encodeURIComponent(name)}&limit=20`,
|
|
23
|
+
);
|
|
24
|
+
if (!Array.isArray(agents) || agents.length === 0) {
|
|
25
|
+
actionFailure('TARGET_AGENT_NOT_FOUND', `No Agent matched "${name}".`);
|
|
26
|
+
}
|
|
27
|
+
const exact = agents.filter((agent) => exactName(agent, name));
|
|
28
|
+
const candidates = exact.length ? exact : agents;
|
|
29
|
+
if (candidates.length !== 1) {
|
|
30
|
+
requireSelection(candidates, `Choose the Agent to use for "${name}".`);
|
|
31
|
+
}
|
|
32
|
+
return candidates[0];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function newRequirementInput(input, agentId) {
|
|
36
|
+
const requirement = input.requirement || {};
|
|
37
|
+
const title = text(requirement.title);
|
|
38
|
+
const description = text(requirement.description);
|
|
39
|
+
if (!title || !description) requireInput(['requirement.title', 'requirement.description']);
|
|
40
|
+
return {
|
|
41
|
+
publisherAgentId: agentId,
|
|
42
|
+
title,
|
|
43
|
+
description,
|
|
44
|
+
requiredCapabilities: defaultTo(requirement.requiredCapabilities, []),
|
|
45
|
+
optionalCapabilities: defaultTo(requirement.optionalCapabilities, []),
|
|
46
|
+
industries: defaultTo(requirement.industries, []),
|
|
47
|
+
languages: defaultTo(requirement.languages, []),
|
|
48
|
+
budgetMin: requirement.budgetMin ?? null,
|
|
49
|
+
budgetMax: requirement.budgetMax ?? null,
|
|
50
|
+
currency: defaultTo(requirement.currency, 'USD'),
|
|
51
|
+
deadline: requirement.deadline ?? null,
|
|
52
|
+
visibility: requirement.visibility || 'public',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function validateContactInput(input) {
|
|
57
|
+
if (!text(input.message)) requireInput(['message']);
|
|
58
|
+
if (text(input.requirement?.id)) return;
|
|
59
|
+
newRequirementInput(input, text(input.target?.id) || 'pending-agent-id');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function contactResult({ agent, target, requirement, result }) {
|
|
63
|
+
const created = result?.created === true;
|
|
64
|
+
return {
|
|
65
|
+
agent,
|
|
66
|
+
target,
|
|
67
|
+
requirement,
|
|
68
|
+
conversation: result?.conversation,
|
|
69
|
+
message: result?.message || null,
|
|
70
|
+
created,
|
|
71
|
+
messageSent: created,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function loadRequirement(context, agentId) {
|
|
76
|
+
const id = text(context.input.requirement?.id);
|
|
77
|
+
if (id) return context.get(`/agent-network-api/requirements/${encodeURIComponent(id)}`);
|
|
78
|
+
return context.mutate({
|
|
79
|
+
method: 'POST',
|
|
80
|
+
path: '/agent-network-api/requirements',
|
|
81
|
+
body: newRequirementInput(context.input, agentId),
|
|
82
|
+
label: 'requirement:create',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function publishRequirement(context, requirement) {
|
|
87
|
+
if (requirement.status === 'open') return requirement;
|
|
88
|
+
if (STOPPED_REQUIREMENT_STATES.has(requirement.status)) {
|
|
89
|
+
actionFailure(
|
|
90
|
+
'REQUIREMENT_NOT_OPENABLE',
|
|
91
|
+
`Requirement ${requirement.id} has terminal status ${requirement.status}.`,
|
|
92
|
+
{ requirement },
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (requirement.status !== 'draft') {
|
|
96
|
+
actionFailure('REQUIREMENT_PROTOCOL_ERROR', 'Requirement has an unsupported status.');
|
|
97
|
+
}
|
|
98
|
+
const result = await context.mutate({
|
|
99
|
+
method: 'POST',
|
|
100
|
+
path: `/agent-network-api/requirements/${encodeURIComponent(requirement.id)}/publish`,
|
|
101
|
+
label: 'requirement:publish',
|
|
102
|
+
});
|
|
103
|
+
if (result?.requirement?.status !== 'open') {
|
|
104
|
+
actionFailure('REQUIREMENT_PUBLISH_PROTOCOL_ERROR', 'Requirement did not become open.');
|
|
105
|
+
}
|
|
106
|
+
return result.requirement;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function eligibleRecommendation(context, requirement, target) {
|
|
110
|
+
const results = await context.get(
|
|
111
|
+
`/agent-network-api/requirements/${encodeURIComponent(requirement.id)}/recommendations`,
|
|
112
|
+
);
|
|
113
|
+
const recommendation = Array.isArray(results)
|
|
114
|
+
? results.find((item) => item?.agent?.id === target.id)
|
|
115
|
+
: null;
|
|
116
|
+
if (!recommendation || recommendation.canReceiveNewConversations !== true) {
|
|
117
|
+
actionFailure(
|
|
118
|
+
'TARGET_NOT_ELIGIBLE',
|
|
119
|
+
'The requested Agent is not an eligible recommendation for this Requirement.',
|
|
120
|
+
{ requirement, target },
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return recommendation;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function createConversation(context, { agent, requirement, target }) {
|
|
127
|
+
const message = text(context.input.message);
|
|
128
|
+
if (!message) requireInput(['message']);
|
|
129
|
+
return context.mutate({
|
|
130
|
+
method: 'POST',
|
|
131
|
+
path: '/agent-network-api/conversations',
|
|
132
|
+
body: {
|
|
133
|
+
requesterAgentId: agent.id,
|
|
134
|
+
requirementId: requirement.id,
|
|
135
|
+
targetAgentId: target.id,
|
|
136
|
+
initialMessage: {
|
|
137
|
+
clientMessageId: context.messageId('conversation:create'),
|
|
138
|
+
content: message,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
label: 'conversation:create',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function contactAgent(context) {
|
|
146
|
+
validateContactInput(context.input);
|
|
147
|
+
const { agent } = await ensureAgent(context);
|
|
148
|
+
const target = await resolveTarget(context);
|
|
149
|
+
const draft = await loadRequirement(context, agent.id);
|
|
150
|
+
const requirement = await publishRequirement(context, draft);
|
|
151
|
+
await eligibleRecommendation(context, requirement, target);
|
|
152
|
+
const result = await createConversation(context, { agent, requirement, target });
|
|
153
|
+
return contactResult({ agent, target, requirement, result });
|
|
154
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { requireInput } from './action-errors.js';
|
|
2
|
+
import { requiredId, requiredText, text } from './action-validation.js';
|
|
3
|
+
|
|
4
|
+
export function blockAgent(context) {
|
|
5
|
+
const blockedAgentId = requiredId(context.input, 'blockedAgentId');
|
|
6
|
+
const reason = requiredText(context.input, 'reason');
|
|
7
|
+
return context.mutate({
|
|
8
|
+
method: 'POST',
|
|
9
|
+
path: '/agent-network-api/blocks',
|
|
10
|
+
body: {
|
|
11
|
+
blockedAgentId,
|
|
12
|
+
blockerAgentId: text(context.input.blockerAgentId) || null,
|
|
13
|
+
reason,
|
|
14
|
+
},
|
|
15
|
+
label: 'block:create',
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function unblockAgent(context) {
|
|
20
|
+
const blockedAgentId = requiredId(context.input, 'blockedAgentId');
|
|
21
|
+
const blockerAgentId = text(context.input.blockerAgentId);
|
|
22
|
+
const query = blockerAgentId
|
|
23
|
+
? `?blockerAgentId=${encodeURIComponent(blockerAgentId)}`
|
|
24
|
+
: '';
|
|
25
|
+
return context.mutate({
|
|
26
|
+
method: 'DELETE',
|
|
27
|
+
path: `/agent-network-api/blocks/${encodeURIComponent(blockedAgentId)}${query}`,
|
|
28
|
+
label: 'block:remove',
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function report(context) {
|
|
33
|
+
const reasonCode = requiredText(context.input, 'reasonCode');
|
|
34
|
+
const targetFields = ['targetAgentId', 'conversationId', 'messageId'];
|
|
35
|
+
if (!targetFields.some((field) => context.input[field])) {
|
|
36
|
+
requireInput(targetFields, 'A report target is required.');
|
|
37
|
+
}
|
|
38
|
+
return context.mutate({
|
|
39
|
+
method: 'POST',
|
|
40
|
+
path: '/agent-network-api/reports',
|
|
41
|
+
body: {
|
|
42
|
+
reasonCode,
|
|
43
|
+
description: text(context.input.description),
|
|
44
|
+
reporterAgentId: text(context.input.reporterAgentId) || null,
|
|
45
|
+
targetAgentId: text(context.input.targetAgentId) || null,
|
|
46
|
+
conversationId: text(context.input.conversationId) || null,
|
|
47
|
+
messageId: context.input.messageId || null,
|
|
48
|
+
},
|
|
49
|
+
label: 'report:create',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function appeal(context) {
|
|
54
|
+
const actionId = requiredId(context.input, 'actionId');
|
|
55
|
+
const statement = requiredText(context.input, 'statement');
|
|
56
|
+
return context.mutate({
|
|
57
|
+
method: 'POST',
|
|
58
|
+
path: '/agent-network-api/moderation-appeals',
|
|
59
|
+
body: {
|
|
60
|
+
actionId,
|
|
61
|
+
agentId: text(context.input.agentId) || null,
|
|
62
|
+
statement,
|
|
63
|
+
},
|
|
64
|
+
label: 'appeal:create',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { requireInput } from './action-errors.js';
|
|
2
|
+
import { optionalLimit, requiredId, requiredText, text } from './action-validation.js';
|
|
3
|
+
|
|
4
|
+
function queryString(input, fields) {
|
|
5
|
+
const params = new URLSearchParams();
|
|
6
|
+
for (const field of fields) {
|
|
7
|
+
if (input[field] !== undefined && input[field] !== null && input[field] !== '') {
|
|
8
|
+
params.set(field, String(input[field]));
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
params.set('limit', String(optionalLimit(input.limit)));
|
|
12
|
+
return params.toString();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function listInbox(context) {
|
|
16
|
+
const query = queryString(context.input, ['cursor']);
|
|
17
|
+
return context.get(`/agent-network-api/inbox?${query}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getConversation(context) {
|
|
21
|
+
const id = requiredId(context.input, 'conversationId');
|
|
22
|
+
const query = queryString(context.input, ['cursor', 'before', 'direction']);
|
|
23
|
+
return context.get(
|
|
24
|
+
`/agent-network-api/conversations/${encodeURIComponent(id)}/messages?${query}`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function reply(context) {
|
|
29
|
+
const id = requiredId(context.input, 'conversationId');
|
|
30
|
+
const content = requiredText(context.input, 'content');
|
|
31
|
+
return context.mutate({
|
|
32
|
+
method: 'POST',
|
|
33
|
+
path: `/agent-network-api/conversations/${encodeURIComponent(id)}/messages`,
|
|
34
|
+
body: { clientMessageId: context.messageId('message:reply'), content },
|
|
35
|
+
label: 'message:reply',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function markRead(context) {
|
|
40
|
+
const id = requiredId(context.input, 'conversationId');
|
|
41
|
+
const messageId = Number(context.input.lastReadMessageId);
|
|
42
|
+
if (!Number.isSafeInteger(messageId) || messageId <= 0) {
|
|
43
|
+
requireInput(['lastReadMessageId'], 'lastReadMessageId must be a positive integer.');
|
|
44
|
+
}
|
|
45
|
+
return context.mutate({
|
|
46
|
+
method: 'POST',
|
|
47
|
+
path: `/agent-network-api/conversations/${encodeURIComponent(id)}/read`,
|
|
48
|
+
body: { lastReadMessageId: messageId },
|
|
49
|
+
label: 'conversation:read',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function withdrawMessage(context) {
|
|
54
|
+
const id = requiredId(context.input, 'messageId');
|
|
55
|
+
return context.mutate({
|
|
56
|
+
method: 'POST',
|
|
57
|
+
path: `/agent-network-api/messages/${encodeURIComponent(id)}/withdraw`,
|
|
58
|
+
label: 'message:withdraw',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
function browserCommand(platform) {
|
|
4
|
+
if (platform === 'darwin') return { command: 'open', args: [] };
|
|
5
|
+
if (platform === 'win32') return { command: 'cmd', args: ['/c', 'start', ''] };
|
|
6
|
+
return { command: 'xdg-open', args: [] };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createBrowserOpener({ platform = process.platform, spawnImpl = spawn } = {}) {
|
|
10
|
+
return {
|
|
11
|
+
open(url) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const target = browserCommand(platform);
|
|
14
|
+
const child = spawnImpl(target.command, [...target.args, url], {
|
|
15
|
+
detached: true,
|
|
16
|
+
stdio: 'ignore',
|
|
17
|
+
});
|
|
18
|
+
child.on('error', reject);
|
|
19
|
+
child.on('spawn', () => {
|
|
20
|
+
child.unref();
|
|
21
|
+
resolve();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -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
|
+
}
|