aegis-desktop 1.0.13 → 1.0.15

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