@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.
@@ -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
+ }