@aura-labs-ai/scout 0.2.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 +237 -0
- package/bin/scout-cli.js +325 -0
- package/examples/basic-purchase.js +112 -0
- package/examples/protocol-integration.js +294 -0
- package/package.json +63 -0
- package/src/activity.js +420 -0
- package/src/ap2/mandates.js +366 -0
- package/src/client.js +217 -0
- package/src/errors.js +66 -0
- package/src/index.js +655 -0
- package/src/intent-session.js +233 -0
- package/src/key-manager.js +204 -0
- package/src/key-manager.test.js +293 -0
- package/src/mcp/client.js +458 -0
- package/src/session.js +603 -0
- package/src/tap/visa.js +345 -0
- package/src/tests/README.md +157 -0
- package/src/tests/ap2-mandates.test.js +413 -0
- package/src/tests/intent-session.test.js +292 -0
- package/src/tests/mcp-client.test.js +224 -0
- package/src/tests/ping.test.js +282 -0
- package/src/tests/run-protocol-tests.js +57 -0
- package/src/tests/scenarios/ap2-scenarios.test.js +422 -0
- package/src/tests/scenarios/index.js +60 -0
- package/src/tests/scenarios/integration-scenarios.test.js +514 -0
- package/src/tests/scenarios/mcp-scenarios.test.js +419 -0
- package/src/tests/scenarios/tap-scenarios.test.js +461 -0
- package/src/tests/visa-tap.test.js +376 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IntentSession — Conversational Intent Completeness Checker
|
|
3
|
+
*
|
|
4
|
+
* Accumulates natural language text across multiple submit() calls,
|
|
5
|
+
* checks completeness via @aura-labs-ai/nlp, and generates clarification
|
|
6
|
+
* questions for missing categories.
|
|
7
|
+
*
|
|
8
|
+
* Used by Scout SDK's createIntentSession() to validate intent
|
|
9
|
+
* completeness locally before sending to Core.
|
|
10
|
+
*
|
|
11
|
+
* State machine:
|
|
12
|
+
* idle → checking → incomplete → checking → ... → complete
|
|
13
|
+
*
|
|
14
|
+
* Design:
|
|
15
|
+
* - Presence detection only (not semantic authority — that's Core's job)
|
|
16
|
+
* - Works with or without an LLM provider (regex-only fallback)
|
|
17
|
+
* - Produces a completeness attestation for Core (informational, not authoritative)
|
|
18
|
+
* - Max 2 clarification questions per round (per @aura-labs-ai/nlp design)
|
|
19
|
+
*
|
|
20
|
+
* Ref: ADR-002, DEC-024, DEC-025, NEUTRAL_BROKER.md Property 1
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { checkCompleteness } from '@aura-labs-ai/nlp';
|
|
24
|
+
import { generateClarification } from '@aura-labs-ai/nlp';
|
|
25
|
+
|
|
26
|
+
// ── IntentSession ────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {Object} SubmitResult
|
|
30
|
+
* @property {boolean} complete — Whether the intent is complete enough
|
|
31
|
+
* @property {string[]} missing — Category names still unresolved
|
|
32
|
+
* @property {string} question — Clarification question (empty when complete)
|
|
33
|
+
* @property {number} confidence — 0-1 overall confidence
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Conversational intent session for accumulating and validating
|
|
38
|
+
* intent completeness before submission to Core.
|
|
39
|
+
*/
|
|
40
|
+
class IntentSession {
|
|
41
|
+
#texts = [];
|
|
42
|
+
#state = 'idle'; // idle | checking | incomplete | complete
|
|
43
|
+
#rounds = [];
|
|
44
|
+
#lastResult = null;
|
|
45
|
+
#provider;
|
|
46
|
+
#activityLogger;
|
|
47
|
+
#providerTimeoutMs;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {Object} [options]
|
|
51
|
+
* @param {Object} [options.provider] — LLM provider for model-based detection
|
|
52
|
+
* @param {Object} [options.activityLogger] — Activity logger for events
|
|
53
|
+
* @param {number} [options.providerTimeoutMs=10000] — Timeout for provider calls in ms
|
|
54
|
+
*/
|
|
55
|
+
constructor(options = {}) {
|
|
56
|
+
this.#provider = options.provider || null;
|
|
57
|
+
this.#activityLogger = options.activityLogger || null;
|
|
58
|
+
this.#providerTimeoutMs = options.providerTimeoutMs || 10_000;
|
|
59
|
+
|
|
60
|
+
this.#emitEvent('intent_session.created', {});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Submit text to the session. Accumulates with previous text
|
|
65
|
+
* and checks completeness.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} text — Additional intent text from the user
|
|
68
|
+
* @returns {Promise<SubmitResult>}
|
|
69
|
+
*/
|
|
70
|
+
async submit(text) {
|
|
71
|
+
// If already complete, return cached result
|
|
72
|
+
if (this.#state === 'complete' && this.#lastResult) {
|
|
73
|
+
return this.#lastResult;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Accumulate text
|
|
77
|
+
const trimmed = (text || '').trim();
|
|
78
|
+
if (trimmed.length > 0) {
|
|
79
|
+
this.#texts.push(trimmed);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Build the full accumulated text
|
|
83
|
+
const fullText = this.getText();
|
|
84
|
+
|
|
85
|
+
this.#state = 'checking';
|
|
86
|
+
this.#emitEvent('intent_session.submitted', { text: trimmed, roundNumber: this.#rounds.length + 1 });
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
// Check completeness via @aura-labs-ai/nlp with timeout protection.
|
|
90
|
+
// Provider calls (remote LLM) can hang — enforce a ceiling.
|
|
91
|
+
const completenessPromise = checkCompleteness(fullText, {
|
|
92
|
+
provider: this.#provider,
|
|
93
|
+
});
|
|
94
|
+
const completenessResult = await Promise.race([
|
|
95
|
+
completenessPromise,
|
|
96
|
+
new Promise((_, reject) =>
|
|
97
|
+
setTimeout(() => reject(new Error('Provider timeout')), this.#providerTimeoutMs),
|
|
98
|
+
),
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
// Generate clarification question for missing categories
|
|
102
|
+
const clarification = generateClarification(
|
|
103
|
+
completenessResult.missing,
|
|
104
|
+
{ previousRounds: this.#rounds },
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// Record this round
|
|
108
|
+
this.#rounds.push({
|
|
109
|
+
categories_addressed: this.#getAddressedCategories(completenessResult),
|
|
110
|
+
userResponse: trimmed,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Build result
|
|
114
|
+
const result = {
|
|
115
|
+
complete: completenessResult.complete,
|
|
116
|
+
missing: completenessResult.missing,
|
|
117
|
+
question: clarification.question || '',
|
|
118
|
+
confidence: completenessResult.confidence,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Update state
|
|
122
|
+
this.#state = completenessResult.complete ? 'complete' : 'incomplete';
|
|
123
|
+
this.#lastResult = result;
|
|
124
|
+
|
|
125
|
+
this.#emitEvent(
|
|
126
|
+
completenessResult.complete ? 'intent_session.complete' : 'intent_session.incomplete',
|
|
127
|
+
{
|
|
128
|
+
complete: completenessResult.complete,
|
|
129
|
+
missing: completenessResult.missing,
|
|
130
|
+
confidence: completenessResult.confidence,
|
|
131
|
+
roundNumber: this.#rounds.length,
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
return result;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
// Graceful degradation — return incomplete with error info
|
|
138
|
+
this.#state = 'incomplete';
|
|
139
|
+
const result = {
|
|
140
|
+
complete: false,
|
|
141
|
+
missing: ['unknown'],
|
|
142
|
+
question: 'Could you provide more details about your requirements?',
|
|
143
|
+
confidence: 0,
|
|
144
|
+
};
|
|
145
|
+
this.#lastResult = result;
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get the completeness attestation for Core.
|
|
152
|
+
* Returns null if the session is not yet complete.
|
|
153
|
+
*
|
|
154
|
+
* This attestation is informational only — Core retains exclusive
|
|
155
|
+
* semantic authority per NEUTRAL_BROKER.md Property 1.
|
|
156
|
+
*
|
|
157
|
+
* @returns {Object|null}
|
|
158
|
+
*/
|
|
159
|
+
getAttestation() {
|
|
160
|
+
if (this.#state !== 'complete' || !this.#lastResult) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
complete: true,
|
|
166
|
+
missing: this.#lastResult.missing,
|
|
167
|
+
confidence: this.#lastResult.confidence,
|
|
168
|
+
rounds: this.#rounds.length,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get the full accumulated text.
|
|
174
|
+
* @returns {string}
|
|
175
|
+
*/
|
|
176
|
+
getText() {
|
|
177
|
+
return this.#texts.join(' ');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get the current state.
|
|
182
|
+
* @returns {'idle'|'checking'|'incomplete'|'complete'}
|
|
183
|
+
*/
|
|
184
|
+
get state() {
|
|
185
|
+
return this.#state;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get the number of rounds completed.
|
|
190
|
+
* @returns {number}
|
|
191
|
+
*/
|
|
192
|
+
get roundCount() {
|
|
193
|
+
return this.#rounds.length;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Reset the session to idle state.
|
|
198
|
+
*/
|
|
199
|
+
reset() {
|
|
200
|
+
this.#texts = [];
|
|
201
|
+
this.#state = 'idle';
|
|
202
|
+
this.#rounds = [];
|
|
203
|
+
this.#lastResult = null;
|
|
204
|
+
this.#emitEvent('intent_session.reset', {});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── Private ──────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Determine which categories were addressed in this round
|
|
211
|
+
* by looking at what's now present.
|
|
212
|
+
*/
|
|
213
|
+
#getAddressedCategories(completenessResult) {
|
|
214
|
+
const addressed = [];
|
|
215
|
+
for (const [name, cat] of Object.entries(completenessResult.categories)) {
|
|
216
|
+
if (cat.present) {
|
|
217
|
+
addressed.push(name);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return addressed;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Emit an activity event if logger is available.
|
|
225
|
+
*/
|
|
226
|
+
#emitEvent(type, data) {
|
|
227
|
+
if (this.#activityLogger && typeof this.#activityLogger.record === 'function') {
|
|
228
|
+
this.#activityLogger.record(type, data);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export { IntentSession };
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Key Manager — Ed25519 Identity Management
|
|
3
|
+
*
|
|
4
|
+
* Manages the agent's root Ed25519 key pair, which is the foundation
|
|
5
|
+
* of its cryptographic identity in the AURA network.
|
|
6
|
+
*
|
|
7
|
+
* Key responsibilities:
|
|
8
|
+
* - Generate or load the root key pair
|
|
9
|
+
* - Sign data (registration proofs, request signatures)
|
|
10
|
+
* - Build canonical signing strings for HTTP request authentication
|
|
11
|
+
* - Provide pluggable storage adapters for persistence across restarts
|
|
12
|
+
*
|
|
13
|
+
* Key format: raw 32-byte Ed25519 keys, base64-encoded (compatible with
|
|
14
|
+
* tweetnacl.js in browser contexts and Node.js crypto module).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import crypto from 'node:crypto';
|
|
18
|
+
import { MemoryStorage } from '@aura-labs-ai/sdk-common';
|
|
19
|
+
|
|
20
|
+
// Re-export for backward compatibility
|
|
21
|
+
export { MemoryStorage };
|
|
22
|
+
|
|
23
|
+
// Ed25519 DER prefixes for raw key <-> KeyObject conversion
|
|
24
|
+
const SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex'); // public key
|
|
25
|
+
const PKCS8_PREFIX = Buffer.from('302e020100300506032b657004220420', 'hex'); // private key
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* KeyManager — Manages an agent's Ed25519 root key pair
|
|
29
|
+
*
|
|
30
|
+
* @param {object} options
|
|
31
|
+
* @param {object} [options.storage] - Storage adapter with get/set/remove methods
|
|
32
|
+
* @param {string} [options.storagePrefix] - Key prefix for storage (default: 'aura:agent:')
|
|
33
|
+
*/
|
|
34
|
+
export class KeyManager {
|
|
35
|
+
#storage;
|
|
36
|
+
#prefix;
|
|
37
|
+
#publicKeyBase64 = null;
|
|
38
|
+
#privateKeyObject = null;
|
|
39
|
+
#publicKeyObject = null;
|
|
40
|
+
#initialized = false;
|
|
41
|
+
|
|
42
|
+
constructor({ storage, storagePrefix } = {}) {
|
|
43
|
+
this.#storage = storage || new MemoryStorage();
|
|
44
|
+
this.#prefix = storagePrefix || 'aura:agent:';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Initialize the key manager — load existing keys or generate new ones
|
|
49
|
+
*
|
|
50
|
+
* @returns {Promise<{ publicKey: string, isNew: boolean }>}
|
|
51
|
+
* publicKey is base64-encoded raw 32-byte Ed25519 public key
|
|
52
|
+
* isNew is true if a new key pair was generated
|
|
53
|
+
*/
|
|
54
|
+
async init() {
|
|
55
|
+
if (this.#initialized) {
|
|
56
|
+
return { publicKey: this.#publicKeyBase64, isNew: false };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Try to load existing keys from storage
|
|
60
|
+
const storedPublic = await this.#storage.get(`${this.#prefix}publicKey`);
|
|
61
|
+
const storedPrivate = await this.#storage.get(`${this.#prefix}privateKey`);
|
|
62
|
+
|
|
63
|
+
if (storedPublic && storedPrivate) {
|
|
64
|
+
this.#publicKeyBase64 = storedPublic;
|
|
65
|
+
this.#publicKeyObject = this.#rawPublicToKeyObject(storedPublic);
|
|
66
|
+
this.#privateKeyObject = this.#rawPrivateToKeyObject(storedPrivate);
|
|
67
|
+
this.#initialized = true;
|
|
68
|
+
return { publicKey: this.#publicKeyBase64, isNew: false };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Generate new key pair
|
|
72
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
|
|
73
|
+
|
|
74
|
+
// Extract raw bytes for storage and interop
|
|
75
|
+
const spkiDer = publicKey.export({ type: 'spki', format: 'der' });
|
|
76
|
+
const rawPublic = spkiDer.subarray(spkiDer.length - 32);
|
|
77
|
+
this.#publicKeyBase64 = rawPublic.toString('base64');
|
|
78
|
+
|
|
79
|
+
const pkcs8Der = privateKey.export({ type: 'pkcs8', format: 'der' });
|
|
80
|
+
const rawPrivate = pkcs8Der.subarray(pkcs8Der.length - 32);
|
|
81
|
+
const rawPrivateBase64 = rawPrivate.toString('base64');
|
|
82
|
+
|
|
83
|
+
// Store for persistence
|
|
84
|
+
await this.#storage.set(`${this.#prefix}publicKey`, this.#publicKeyBase64);
|
|
85
|
+
await this.#storage.set(`${this.#prefix}privateKey`, rawPrivateBase64);
|
|
86
|
+
|
|
87
|
+
this.#publicKeyObject = publicKey;
|
|
88
|
+
this.#privateKeyObject = privateKey;
|
|
89
|
+
this.#initialized = true;
|
|
90
|
+
|
|
91
|
+
return { publicKey: this.#publicKeyBase64, isNew: true };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Sign arbitrary data with the agent's private key
|
|
96
|
+
*
|
|
97
|
+
* @param {string} data - String data to sign
|
|
98
|
+
* @returns {string} Base64-encoded Ed25519 signature (64 bytes)
|
|
99
|
+
* @throws {Error} If not initialized
|
|
100
|
+
*/
|
|
101
|
+
sign(data) {
|
|
102
|
+
this.#ensureInitialized();
|
|
103
|
+
const signature = crypto.sign(null, Buffer.from(data), this.#privateKeyObject);
|
|
104
|
+
return signature.toString('base64');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Build and sign the canonical request string for HTTP request authentication
|
|
109
|
+
*
|
|
110
|
+
* Format: "${method}\n${path}\n${timestamp}\n${bodyDigest}"
|
|
111
|
+
*
|
|
112
|
+
* @param {object} params
|
|
113
|
+
* @param {string} params.method - HTTP method (uppercase)
|
|
114
|
+
* @param {string} params.path - Request path (e.g., /sessions)
|
|
115
|
+
* @param {string|null} params.body - JSON body string or null
|
|
116
|
+
* @returns {{ signature: string, timestamp: string }} Signature and timestamp to include in headers
|
|
117
|
+
*/
|
|
118
|
+
signRequest({ method, path, body }) {
|
|
119
|
+
this.#ensureInitialized();
|
|
120
|
+
|
|
121
|
+
const timestamp = Date.now().toString();
|
|
122
|
+
const bodyDigest = body
|
|
123
|
+
? crypto.createHash('sha256').update(body).digest('base64')
|
|
124
|
+
: '';
|
|
125
|
+
const signingString = `${method}\n${path}\n${timestamp}\n${bodyDigest}`;
|
|
126
|
+
const signature = this.sign(signingString);
|
|
127
|
+
|
|
128
|
+
return { signature, timestamp };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get the public key (base64-encoded raw 32-byte Ed25519)
|
|
133
|
+
*
|
|
134
|
+
* @returns {string|null} Public key or null if not initialized
|
|
135
|
+
*/
|
|
136
|
+
get publicKey() {
|
|
137
|
+
return this.#publicKeyBase64;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check if the key manager has been initialized
|
|
142
|
+
*
|
|
143
|
+
* @returns {boolean}
|
|
144
|
+
*/
|
|
145
|
+
get isInitialized() {
|
|
146
|
+
return this.#initialized;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Compute the key fingerprint (SHA-256 hex of raw public key)
|
|
151
|
+
*
|
|
152
|
+
* @returns {string} 64-char hex fingerprint
|
|
153
|
+
*/
|
|
154
|
+
get fingerprint() {
|
|
155
|
+
this.#ensureInitialized();
|
|
156
|
+
return crypto.createHash('sha256')
|
|
157
|
+
.update(Buffer.from(this.#publicKeyBase64, 'base64'))
|
|
158
|
+
.digest('hex');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Load the stored agentId from storage
|
|
163
|
+
*
|
|
164
|
+
* @returns {Promise<string|null>}
|
|
165
|
+
*/
|
|
166
|
+
async getAgentId() {
|
|
167
|
+
return this.#storage.get(`${this.#prefix}agentId`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Store the agentId after registration
|
|
172
|
+
*
|
|
173
|
+
* @param {string} agentId - UUID from /agents/register response
|
|
174
|
+
*/
|
|
175
|
+
async setAgentId(agentId) {
|
|
176
|
+
await this.#storage.set(`${this.#prefix}agentId`, agentId);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ─── Private helpers ────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
#ensureInitialized() {
|
|
182
|
+
if (!this.#initialized) {
|
|
183
|
+
throw new Error('KeyManager not initialized. Call init() first.');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Convert raw 32-byte base64 public key to Node.js KeyObject
|
|
189
|
+
*/
|
|
190
|
+
#rawPublicToKeyObject(base64Key) {
|
|
191
|
+
const rawBytes = Buffer.from(base64Key, 'base64');
|
|
192
|
+
const derKey = Buffer.concat([SPKI_PREFIX, rawBytes]);
|
|
193
|
+
return crypto.createPublicKey({ key: derKey, format: 'der', type: 'spki' });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Convert raw 32-byte base64 private key (seed) to Node.js KeyObject
|
|
198
|
+
*/
|
|
199
|
+
#rawPrivateToKeyObject(base64Key) {
|
|
200
|
+
const rawBytes = Buffer.from(base64Key, 'base64');
|
|
201
|
+
const derKey = Buffer.concat([PKCS8_PREFIX, rawBytes]);
|
|
202
|
+
return crypto.createPrivateKey({ key: derKey, format: 'der', type: 'pkcs8' });
|
|
203
|
+
}
|
|
204
|
+
}
|