aegis-desktop 0.8.12 → 0.8.13
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/lib/auto.js +423 -0
- package/lib/avatar/packs/core-wardrobe.pack.json +1 -1
- package/lib/avatar/packs/local-voice-core.pack.json +1 -1
- package/lib/avatar/packs/seasonal-winter.pack.json +1 -1
- package/lib/avatar/profile.js +10 -1
- package/lib/fingerprint.js +327 -0
- package/lib/local/autonomous.js +1 -0
- package/lib/local/prompt.js +6 -0
- package/lib/settings.js +154 -1
- package/main.js +316 -17
- package/package.json +2 -2
- package/preload.js +84 -4
- package/renderer/app.js +208 -4
- package/renderer/fingerprint.css +143 -0
- package/renderer/fingerprint.js +583 -0
- package/renderer/index.html +163 -1
- package/vendor/aegis.js +180 -18
- package/vendor/attest.js +439 -0
- package/vendor/authorship-key.js +366 -0
- package/vendor/device-fingerprint.js +85 -3
- package/vendor/fingerprint.js +1061 -0
- package/vendor/presence-constants.js +104 -0
- package/vendor/presence.js +435 -0
- package/vendor/record-vault.js +834 -0
- package/vendor/touch.js +455 -0
package/vendor/attest.js
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* attest.js — turn a session into a φ(α) record.
|
|
5
|
+
*
|
|
6
|
+
* `client/fingerprint.js` knows how to seal a record from *inputs*; it knows
|
|
7
|
+
* nothing about where those inputs live, and that is deliberate — the vendored
|
|
8
|
+
* module must stay byte-identical to the engine's, so nothing host-specific can
|
|
9
|
+
* be added to it. This file is that missing half, shared by the CLI and the
|
|
10
|
+
* desktop so both hosts attest a session the same way: the same log is read the
|
|
11
|
+
* same way, thumbed into the same observations, and sealed under the same key.
|
|
12
|
+
*
|
|
13
|
+
* ## What the fields may be
|
|
14
|
+
*
|
|
15
|
+
* The engine's rule (see `src/fingerprint.js`) is that a record may carry only
|
|
16
|
+
* what was actually observed: a fabricated field is not evidence. Carried over
|
|
17
|
+
* literally here, that means this module never invents a value to fill a shape:
|
|
18
|
+
*
|
|
19
|
+
* - a session log entry that carries no recognisable working directory
|
|
20
|
+
* contributes `dir: null`, not `process.cwd()`;
|
|
21
|
+
* - a turn with no recorded outcome contributes `outcome: null`, not `'ok'`;
|
|
22
|
+
* - `--turns N` that selects more turns than exist attests the turns there
|
|
23
|
+
* are, and the record's `behaviourSamples` then says how many that was.
|
|
24
|
+
*
|
|
25
|
+
* A null field is a true statement about the log ("the log did not say"); a
|
|
26
|
+
* guessed one is a false statement about the session. The engine's `verify`
|
|
27
|
+
* compares the attestation over these fields, so the difference is not
|
|
28
|
+
* cosmetic — it is the difference between a record that reproduces and one
|
|
29
|
+
* that does not.
|
|
30
|
+
*
|
|
31
|
+
* ## Schema tolerance, and why it stops where it does
|
|
32
|
+
*
|
|
33
|
+
* The session log has carried more than one entry shape across versions, so
|
|
34
|
+
* each turn is read through a small ordered list of aliases (`cwd`/`dir`,
|
|
35
|
+
* `demand`/`prompt`/…). Unknown keys are ignored rather than merged in, because
|
|
36
|
+
* an unknown key is a key nobody has agreed on the meaning of, and including it
|
|
37
|
+
* would make the behavioural axis change silently the first time a log gained a
|
|
38
|
+
* field. Adding an alias here is a deliberate act with a test behind it.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
const crypto = require('node:crypto');
|
|
42
|
+
const fs = require('node:fs');
|
|
43
|
+
const path = require('node:path');
|
|
44
|
+
|
|
45
|
+
const fp = require('./fingerprint.js');
|
|
46
|
+
const authorship = require('./authorship-key.js');
|
|
47
|
+
|
|
48
|
+
const DEFAULT_HISTORY = () => path.join(authorship.dataDir(), 'history.jsonl');
|
|
49
|
+
|
|
50
|
+
/** Ordered aliases. First present key wins; a later alias never overrides one. */
|
|
51
|
+
const DIR_KEYS = ['cwd', 'dir', 'workspace', 'root'];
|
|
52
|
+
const DEMAND_KEYS = ['demand', 'prompt', 'task', 'text', 'content'];
|
|
53
|
+
const OUTCOME_KEYS = ['outcome', 'status', 'result'];
|
|
54
|
+
const SESSION_KEYS = ['session', 'sessionId', 'session_id'];
|
|
55
|
+
const TIME_KEYS = ['at', 'ts', 'time', 'createdAt'];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The AI half's aliases. Separate lists from the human half's on purpose: `text`
|
|
59
|
+
* is a demand in `DEMAND_KEYS` and a reply would be one here, and a shared list
|
|
60
|
+
* would let one axis read the other's field — which is exactly the conflation the
|
|
61
|
+
* two signatures exist to prevent.
|
|
62
|
+
*/
|
|
63
|
+
const REPLY_KEYS = ['reply', 'answer', 'response'];
|
|
64
|
+
const MODEL_KEYS = ['model', 'aiModel', 'modelId'];
|
|
65
|
+
|
|
66
|
+
/** `sha256(text)[0:8]` — a content address, never the content. */
|
|
67
|
+
const sha256hex = (text) => crypto.createHash('sha256').update(String(text || ''), 'utf8').digest('hex');
|
|
68
|
+
|
|
69
|
+
/** Where the AI credential comes from: `--ai-secret FILE`, then `AEGIS_AI_SECRET`. */
|
|
70
|
+
const AI_SECRET_ENV = 'AEGIS_AI_SECRET';
|
|
71
|
+
|
|
72
|
+
function firstKey(entry, keys) {
|
|
73
|
+
for (const k of keys) {
|
|
74
|
+
const v = entry[k];
|
|
75
|
+
if (v === undefined || v === null) continue;
|
|
76
|
+
const s = typeof v === 'string' ? v : String(v);
|
|
77
|
+
if (s.trim() !== '') return s;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Read a session log.
|
|
84
|
+
*
|
|
85
|
+
* A malformed line is skipped, not fatal: a half-written last line is the
|
|
86
|
+
* normal state of an append-only log that was interrupted, and refusing the
|
|
87
|
+
* whole file over it would mean the sessions with the most history — the ones
|
|
88
|
+
* worth attesting — are the ones that cannot be.
|
|
89
|
+
*/
|
|
90
|
+
function readHistory(file) {
|
|
91
|
+
if (!fs.existsSync(file)) throw new Error(`attest: no session log at ${file}`);
|
|
92
|
+
const entries = [];
|
|
93
|
+
let skipped = 0;
|
|
94
|
+
for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
|
|
95
|
+
const text = line.trim();
|
|
96
|
+
if (!text) continue;
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(text);
|
|
99
|
+
if (parsed && typeof parsed === 'object') entries.push(parsed);
|
|
100
|
+
else skipped += 1;
|
|
101
|
+
} catch {
|
|
102
|
+
skipped += 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { entries, skipped };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The last session id the log mentions, which is what "current" means. */
|
|
109
|
+
function lastSessionId(entries) {
|
|
110
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
111
|
+
const id = firstKey(entries[i], SESSION_KEYS);
|
|
112
|
+
if (id) return id;
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Select the turns to attest.
|
|
119
|
+
*
|
|
120
|
+
* With no `session`, the last session in the log is used — attributed rather
|
|
121
|
+
* than assumed: turns whose session id is missing are included only when no
|
|
122
|
+
* session was requested, since "which session is this?" is exactly the question
|
|
123
|
+
* they cannot answer.
|
|
124
|
+
*/
|
|
125
|
+
function selectTurns(entries, { session = null, turns = null } = {}) {
|
|
126
|
+
let chosen = entries;
|
|
127
|
+
if (session) {
|
|
128
|
+
const wanted = String(session);
|
|
129
|
+
chosen = entries.filter((e) => firstKey(e, SESSION_KEYS) === wanted);
|
|
130
|
+
}
|
|
131
|
+
if (turns && Number.isFinite(Number(turns)) && Number(turns) > 0) {
|
|
132
|
+
chosen = chosen.slice(-Math.floor(Number(turns)));
|
|
133
|
+
}
|
|
134
|
+
return chosen;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Fold turns into observations.
|
|
139
|
+
*
|
|
140
|
+
* The keys are the ENGINE's (`fp.observationKey` reads `tool`, `target`,
|
|
141
|
+
* `outcome`, `model`) and nothing else, because an observation is hashed by
|
|
142
|
+
* reading those four fields and a field with another name is silently dropped —
|
|
143
|
+
* it never reaches the signature it was handed over to feed. An earlier version of
|
|
144
|
+
* this function emitted `{dir, demand, outcome}` and read as if the demand were
|
|
145
|
+
* attested; it was not, so the behavioural axis of every record minted from a
|
|
146
|
+
* session log described the turn outcomes alone.
|
|
147
|
+
*
|
|
148
|
+
* So the two true things this log says about a turn are carried as the engine's
|
|
149
|
+
* fields: where it ran and what was asked for. `target` is a content address of
|
|
150
|
+
* the demand rather than the demand itself — the signature exists to compare
|
|
151
|
+
* agents, not to archive what they were told — and it is addressed exactly as
|
|
152
|
+
* `aegiscodex`'s own tool addresses it, so the two hosts read one session log into
|
|
153
|
+
* one digest.
|
|
154
|
+
*
|
|
155
|
+
* `outcome` stays `null` when the log did not record one, and a null field
|
|
156
|
+
* contributes no signal: `behaviouralSignature` drops an observation that carries
|
|
157
|
+
* nothing, and `behaviourSamples` counts what was actually attested.
|
|
158
|
+
*/
|
|
159
|
+
function observationsFromTurns(turns) {
|
|
160
|
+
const out = [];
|
|
161
|
+
for (const t of Array.isArray(turns) ? turns : []) {
|
|
162
|
+
if (!t || typeof t !== 'object') continue;
|
|
163
|
+
const dir = firstKey(t, DIR_KEYS);
|
|
164
|
+
const demand = firstKey(t, DEMAND_KEYS);
|
|
165
|
+
out.push({
|
|
166
|
+
tool: demand || dir ? 'turn' : null,
|
|
167
|
+
target: `${dir ? path.basename(dir) : '?'}#${sha256hex(demand || '').slice(0, 8)}`,
|
|
168
|
+
outcome: firstKey(t, OUTCOME_KEYS),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Fold the same turns from the *reply* side: the AI axis's observations.
|
|
176
|
+
*
|
|
177
|
+
* One observation per turn, carrying only what the entry records about the
|
|
178
|
+
* answer — the tool it named (usually none), the working directory it ran in, the
|
|
179
|
+
* outcome, and the model that produced it. Every one of those is `null` when the
|
|
180
|
+
* log did not say, and `null` contributes no signal: `fp.aiSignature` hashes only
|
|
181
|
+
* the observations that carry something, and `aiSamples` counts those. A log that
|
|
182
|
+
* records nothing about the answers therefore yields `ai: null, aiSamples: 0` —
|
|
183
|
+
* "the log did not say" — rather than a signature over a row of empty fields.
|
|
184
|
+
*
|
|
185
|
+
* The reply text is deliberately NOT hashed. It would be a transcript hash wearing
|
|
186
|
+
* a behavioural signature's name: two agents answering the same demand in
|
|
187
|
+
* different words would stop matching, and the axis is meant to compare habits,
|
|
188
|
+
* not prose. `--ai-observations` (the engine CLI) is where a caller who has real
|
|
189
|
+
* tool-level AI observations passes them.
|
|
190
|
+
*/
|
|
191
|
+
function aiObservationsFromTurns(turns) {
|
|
192
|
+
const out = [];
|
|
193
|
+
for (const t of Array.isArray(turns) ? turns : []) {
|
|
194
|
+
if (!t || typeof t !== 'object') continue;
|
|
195
|
+
const reply = firstKey(t, REPLY_KEYS);
|
|
196
|
+
const status = firstKey(t, OUTCOME_KEYS);
|
|
197
|
+
const model = firstKey(t, MODEL_KEYS);
|
|
198
|
+
// "Answered" is evidence about the ANSWER. A turn the log recorded with no
|
|
199
|
+
// reply, no status and no model contributes an all-null observation, which
|
|
200
|
+
// `fp.aiSignature` drops: "the log never said" must stay distinct from a
|
|
201
|
+
// signature over a pretend answer, and the working directory the human side
|
|
202
|
+
// already carries must not be re-read as if it were the AI's evidence.
|
|
203
|
+
const answered = Boolean(reply || status || model);
|
|
204
|
+
out.push({
|
|
205
|
+
tool: answered ? 'reply' : null,
|
|
206
|
+
// The reply is addressed, not archived — the same discipline the engine tool
|
|
207
|
+
// uses for a demand, and it is why both hosts read one log into one digest.
|
|
208
|
+
target: answered ? `reply#${sha256hex(reply || '').slice(0, 8)}` : null,
|
|
209
|
+
outcome: status || null,
|
|
210
|
+
model,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Resolve the AI credential: `--ai-secret FILE`, then `$AEGIS_AI_SECRET`, else no
|
|
218
|
+
* key.
|
|
219
|
+
*
|
|
220
|
+
* `--ai-secret` is an alias for the engine's `--ai-secret-file` spelling, and both
|
|
221
|
+
* are checked because the two hosts' users read different docs. The authorship
|
|
222
|
+
* ladder is NOT consulted: `keyFingerprint` of the sealing key would make every
|
|
223
|
+
* record claim it ran on its author's key, and "which mind answered" would stop
|
|
224
|
+
* being a question with an answer. No key nominated ⇒ `null`, which the record
|
|
225
|
+
* states plainly.
|
|
226
|
+
*/
|
|
227
|
+
function resolveAiSecret(o = {}) {
|
|
228
|
+
const argv = o.argv || [];
|
|
229
|
+
const env = o.env || {};
|
|
230
|
+
const file = firstFlag(argv, ['--ai-secret', '--ai-secret-file']);
|
|
231
|
+
if (file) {
|
|
232
|
+
const secret = fs.readFileSync(file, 'utf8').trim();
|
|
233
|
+
if (!secret) throw new Error(`attest: ${file} is empty — an empty AI credential is not one`);
|
|
234
|
+
return { secret, source: file };
|
|
235
|
+
}
|
|
236
|
+
const envSecret = env[AI_SECRET_ENV] && String(env[AI_SECRET_ENV]).trim();
|
|
237
|
+
if (envSecret) return { secret: envSecret, source: AI_SECRET_ENV };
|
|
238
|
+
return { secret: null, source: 'none' };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** `--flag V`, first spelling that matches. Local so this file keeps no argv helper. */
|
|
242
|
+
function firstFlag(argv, names) {
|
|
243
|
+
for (const name of names) {
|
|
244
|
+
const i = argv.indexOf(name);
|
|
245
|
+
if (i !== -1 && argv[i + 1] !== undefined && !String(argv[i + 1]).startsWith('--')) {
|
|
246
|
+
return argv[i + 1];
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Walk a directory and return `{path, content}` records for the code axis.
|
|
254
|
+
*
|
|
255
|
+
* The key names are the engine's (`codeDigest` reads `f.path`), not this
|
|
256
|
+
* module's choice: a mismatch here yields `codeFiles: 0` and a null axis —
|
|
257
|
+
* silently, since the engine treats an unusable path as "not a file" rather
|
|
258
|
+
* than as an error. `test/fingerprint.test.mjs` pins the count for that reason.
|
|
259
|
+
*
|
|
260
|
+
* Symlinks are not followed and the noisy trees are skipped, matching the
|
|
261
|
+
* engine tool: the axis is meant to say *which revision of which files*, and a
|
|
262
|
+
* walk that followed a symlink out of the declared root, or dragged in
|
|
263
|
+
* `node_modules`, would attest something other than the user's code. An
|
|
264
|
+
* oversized file throws rather than attesting a hole in the digest.
|
|
265
|
+
*/
|
|
266
|
+
const SKIP_DIRS = new Set([
|
|
267
|
+
'.git',
|
|
268
|
+
'node_modules',
|
|
269
|
+
'dist',
|
|
270
|
+
'build',
|
|
271
|
+
'out',
|
|
272
|
+
'vendor',
|
|
273
|
+
'.venv',
|
|
274
|
+
'venv',
|
|
275
|
+
'__pycache__',
|
|
276
|
+
'.next',
|
|
277
|
+
'.cache',
|
|
278
|
+
'release',
|
|
279
|
+
'coverage',
|
|
280
|
+
]);
|
|
281
|
+
const MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
282
|
+
const MAX_FILES = 4096;
|
|
283
|
+
|
|
284
|
+
function collectCode({ roots = [], files = [], skip = [] } = {}) {
|
|
285
|
+
const extraSkip = new Set(skip);
|
|
286
|
+
const out = [];
|
|
287
|
+
const add = (relpath, abs) => {
|
|
288
|
+
const st = fs.statSync(abs);
|
|
289
|
+
if (!st.isFile()) return;
|
|
290
|
+
if (st.size > MAX_FILE_BYTES) {
|
|
291
|
+
throw new Error(`attest: ${abs} is ${st.size} bytes — too large to attest (max ${MAX_FILE_BYTES})`);
|
|
292
|
+
}
|
|
293
|
+
if (out.length >= MAX_FILES) {
|
|
294
|
+
throw new Error(`attest: more than ${MAX_FILES} files — refusing to attest a partial walk`);
|
|
295
|
+
}
|
|
296
|
+
const rel = fp.normaliseCodePath(relpath);
|
|
297
|
+
if (!rel) return; // a path the engine cannot use is not a code file
|
|
298
|
+
out.push({ path: rel, content: fs.readFileSync(abs, 'utf8') });
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
for (const root of roots) {
|
|
302
|
+
const abs = path.resolve(root);
|
|
303
|
+
const walk = (dir, prefix) => {
|
|
304
|
+
for (const name of fs.readdirSync(dir).sort()) {
|
|
305
|
+
const full = path.join(dir, name);
|
|
306
|
+
const rel = prefix ? `${prefix}/${name}` : name;
|
|
307
|
+
const st = fs.lstatSync(full);
|
|
308
|
+
if (st.isSymbolicLink()) continue; // never leave the declared root
|
|
309
|
+
if (st.isDirectory()) {
|
|
310
|
+
if (SKIP_DIRS.has(name) || extraSkip.has(name)) continue;
|
|
311
|
+
walk(full, rel);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
add(rel, full);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
walk(abs, '');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
for (const file of files) {
|
|
321
|
+
const abs = path.resolve(file);
|
|
322
|
+
add(path.basename(abs), abs);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Mint a record for a session.
|
|
330
|
+
*
|
|
331
|
+
* @param {object} o
|
|
332
|
+
* @param {string[]} [o.argv] flags; `--secret`/`--secret-file` are read here
|
|
333
|
+
* @param {object} [o.env]
|
|
334
|
+
* @param {string} [o.history] session log (default `$AEGIS_HOME/history.jsonl`)
|
|
335
|
+
* @param {string} [o.session] session id (default: the last one in the log)
|
|
336
|
+
* @param {number} [o.turns] only the last N turns
|
|
337
|
+
* @param {string} [o.label]
|
|
338
|
+
* @param {string} [o.account]
|
|
339
|
+
* @param {string} [o.model]
|
|
340
|
+
* @param {string[]} [o.tools]
|
|
341
|
+
* @param {object[]} [o.code] output of `collectCode`
|
|
342
|
+
* @param {boolean?} [o.codeDirty]
|
|
343
|
+
* @param {string} [o.createdAt] pin the record's timestamp
|
|
344
|
+
|
|
345
|
+
* `createdAt` is a pass-through to the engine's `mint`, which otherwise stamps
|
|
346
|
+
* the record with the current time. That default is right for a session being
|
|
347
|
+
* sealed now and wrong for the one thing this module is used for besides that:
|
|
348
|
+
* two hosts attesting the *same* session must produce the *same* record, and
|
|
349
|
+
* they cannot if each stampes its own millisecond. It is also what lets a
|
|
350
|
+
* record be re-sealed at the time it originally described. It is deliberately
|
|
351
|
+
* not exposed as a CLI flag — a hand-set timestamp on an ordinary mint would
|
|
352
|
+
* let a caller backdate evidence, which is exactly what the record exists to
|
|
353
|
+
* prevent.
|
|
354
|
+
* @param {string|null} [o.aiSecret] the credential the session ran on. Passed
|
|
355
|
+
* in, resolved from `--ai-secret`/
|
|
356
|
+
* `$AEGIS_AI_SECRET`, or explicitly null.
|
|
357
|
+
* @param {string} [o.aiModel] the model that answered.
|
|
358
|
+
* @param {object[]} [o.aiObservations] the replies' observations; derived from
|
|
359
|
+
* the turns when omitted.
|
|
360
|
+
* @returns {{record: object, keySource: string, public: boolean, samples: number,
|
|
361
|
+
* observations: number, aiKeySource: string, aiSamples: number,
|
|
362
|
+
* skippedLines: number}}
|
|
363
|
+
*/
|
|
364
|
+
function mintSession(o = {}) {
|
|
365
|
+
const key = o.key || authorship.resolveKey(o);
|
|
366
|
+
if (!key.secret) {
|
|
367
|
+
throw new Error(
|
|
368
|
+
'attest: no authorship key — run `aegiscode fingerprint keygen`, set ' +
|
|
369
|
+
`${authorship.SECRET_ENV}, or pass --secret <file>`,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const history = o.history || DEFAULT_HISTORY();
|
|
374
|
+
const { entries, skipped } = readHistory(history);
|
|
375
|
+
const session = o.session || lastSessionId(entries);
|
|
376
|
+
const turns = selectTurns(entries, { session, turns: o.turns });
|
|
377
|
+
const observations = observationsFromTurns(turns);
|
|
378
|
+
|
|
379
|
+
const code = o.code || [];
|
|
380
|
+
|
|
381
|
+
// The AI axis. `aiSecret` may be passed in (a host that already resolved it),
|
|
382
|
+
// left undefined (then the two rungs above are walked), or explicitly null (a
|
|
383
|
+
// host saying "no credential — mint the axis as unobserved").
|
|
384
|
+
const aiKey = o.aiSecret === undefined ? resolveAiSecret(o) : { secret: o.aiSecret, source: o.aiSecret ? 'caller' : 'none' };
|
|
385
|
+
const aiObservations = o.aiObservations || aiObservationsFromTurns(turns);
|
|
386
|
+
const aiModel = o.aiModel !== undefined && o.aiModel !== null
|
|
387
|
+
? o.aiModel
|
|
388
|
+
: aiObservations.map((a) => a.model).filter(Boolean).pop() || '';
|
|
389
|
+
|
|
390
|
+
const record = fp.mint({
|
|
391
|
+
secret: key.secret,
|
|
392
|
+
label: o.label,
|
|
393
|
+
account: o.account,
|
|
394
|
+
model: o.model,
|
|
395
|
+
tools: o.tools,
|
|
396
|
+
observations,
|
|
397
|
+
code,
|
|
398
|
+
codeDirty: o.codeDirty === undefined ? null : o.codeDirty,
|
|
399
|
+
aiSecret: aiKey.secret,
|
|
400
|
+
aiModel,
|
|
401
|
+
aiObservations,
|
|
402
|
+
createdAt: o.createdAt || undefined,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
record,
|
|
407
|
+
keySource: key.source,
|
|
408
|
+
public: key.public,
|
|
409
|
+
samples: record.behaviourSamples,
|
|
410
|
+
observations: observations.length,
|
|
411
|
+
aiKeySource: aiKey.source,
|
|
412
|
+
aiSamples: record.aiSamples,
|
|
413
|
+
skippedLines: skipped,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
module.exports = {
|
|
418
|
+
DEFAULT_HISTORY,
|
|
419
|
+
DIR_KEYS,
|
|
420
|
+
DEMAND_KEYS,
|
|
421
|
+
OUTCOME_KEYS,
|
|
422
|
+
SESSION_KEYS,
|
|
423
|
+
REPLY_KEYS,
|
|
424
|
+
MODEL_KEYS,
|
|
425
|
+
AI_SECRET_ENV,
|
|
426
|
+
TIME_KEYS,
|
|
427
|
+
SKIP_DIRS,
|
|
428
|
+
MAX_FILE_BYTES,
|
|
429
|
+
MAX_FILES,
|
|
430
|
+
firstKey,
|
|
431
|
+
readHistory,
|
|
432
|
+
lastSessionId,
|
|
433
|
+
selectTurns,
|
|
434
|
+
observationsFromTurns,
|
|
435
|
+
aiObservationsFromTurns,
|
|
436
|
+
resolveAiSecret,
|
|
437
|
+
collectCode,
|
|
438
|
+
mintSession,
|
|
439
|
+
};
|