@brunobracaioli/aibrain 0.4.0-beta.1 → 0.4.0-beta.3

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 CHANGED
@@ -14,6 +14,20 @@ both provide the `aisac` and `aibrain` commands. State stays in `~/.aibrain`.
14
14
  shows the answer and sends the request only to the configured AI Brain address.
15
15
  `aisac status` stays offline.
16
16
 
17
+ For licensed accounts, `aisac claude` downloads the AISAC Engineering pack for
18
+ the session, verifies its Ed25519 signature against the key pinned in this
19
+ release, and loads it as the `aisac-engineering` plugin from the session's
20
+ private folder, which is deleted on exit. Any failure starts Claude without the
21
+ pack. `aisac setup --lang pt-BR|en` picks the pack language without a network
22
+ call. The pack is never part of this npm package.
23
+
24
+ `aisac codex` (Codex 0.164.0 or later) loads the same verified pack for that
25
+ session only: its skills are added to the Codex app-server the launch owns
26
+ (call one with `$`, for example `$spec`) and its agents become session agent
27
+ roles. Nothing is written to `~/.codex`, `~/.agents` or the project, and the
28
+ session's folder is deleted on exit. An older Codex starts without the pack and
29
+ says `codex_outdated`.
30
+
17
31
  Codex capture also requires the `flock` utility from `util-linux`, normally
18
32
  included in Linux/WSL. The launcher and doctor check availability before a
19
33
  monitored Codex session starts. Capture coordination is released by the kernel
@@ -54,6 +54,12 @@ const MAX_OWNED_REQUESTS = 1_000;
54
54
  const MAX_RETIRED_REQUESTS = 2_000;
55
55
  const MAX_CLIENT_REQUEST_BINDINGS = 128;
56
56
  const MAX_OWNED_THREAD_IDS = 256;
57
+ // AISAC Engineering (ADR-0047 E4c): the launcher's skills root is added to the
58
+ // owned app-server before the client sees its initialize answer.
59
+ const ENGINEERING_SKILLS_ROOT_ENV = 'AISAC_ENGINEERING_SKILLS_ROOT';
60
+ const ENGINEERING_SKILLS_ACK_TIMEOUT_MS = 2_000;
61
+ const MAX_ENGINEERING_PATH_LENGTH = 4_096;
62
+ const MAX_TRACKED_INITIALIZE_REQUESTS = 4;
57
63
  const MAX_RECOVERY_ITEMS = 128;
58
64
  const MAX_RECOVERY_SOURCE_INDEX = 65_536;
59
65
  const MAX_NOTE_BYTES = 128 * 1024;
@@ -2707,6 +2713,8 @@ export function createCodexContextProxy({
2707
2713
  createDiagnosticSink = null,
2708
2714
  brainServer = process.env.AI_BRAIN_MCP_SERVER ?? 'brain',
2709
2715
  clientVersion = process.env.CODEX_CLIENT_VERSION ?? '0.161.0',
2716
+ engineeringSkillsRoot = null,
2717
+ engineeringAckTimeoutMs = ENGINEERING_SKILLS_ACK_TIMEOUT_MS,
2710
2718
  }) {
2711
2719
  const contextPolicy = normalizeContextPolicy(policy);
2712
2720
  const effectiveThreshold = contextPolicy?.compactAtPercent ?? threshold;
@@ -2719,6 +2727,15 @@ export function createCodexContextProxy({
2719
2727
  const telemetryByThread = new Map();
2720
2728
  const diagnosticSinks = new Map();
2721
2729
  let stopped = false;
2730
+ // AISAC Engineering skills root: 'off' without a root; 'waiting' for the
2731
+ // client's first successful initialize; 'holding' app-server output until
2732
+ // skills/extraRoots/set is answered (or times out); then 'done'.
2733
+ let engineeringState =
2734
+ typeof engineeringSkillsRoot === 'string' && engineeringSkillsRoot.length > 0
2735
+ ? 'waiting'
2736
+ : 'off';
2737
+ const initializeRequestKeys = new Set();
2738
+ let engineeringHeld = null;
2722
2739
  function emitDiagnostic(event) {
2723
2740
  try {
2724
2741
  const phase = typeof event?.phase === 'string' ? event.phase : '';
@@ -4131,6 +4148,11 @@ export function createCodexContextProxy({
4131
4148
  const owned = ownedRequests.get(message.id);
4132
4149
  ownedRequests.delete(message.id);
4133
4150
  retire(message.id);
4151
+ if (owned.kind === 'engineering') {
4152
+ // An error leaves the session without the pack skills; it is never shown.
4153
+ releaseEngineering();
4154
+ return false;
4155
+ }
4134
4156
  if (
4135
4157
  owned.kind === 'read' ||
4136
4158
  owned.kind === 'page' ||
@@ -4364,6 +4386,14 @@ export function createCodexContextProxy({
4364
4386
  if (!validRequestId) return;
4365
4387
  const requestKey = clientRequestKey(requestId);
4366
4388
  if (requestKey === null) return;
4389
+ if (
4390
+ message.method === 'initialize' &&
4391
+ engineeringState === 'waiting' &&
4392
+ initializeRequestKeys.size < MAX_TRACKED_INITIALIZE_REQUESTS
4393
+ ) {
4394
+ initializeRequestKeys.add(requestKey);
4395
+ return;
4396
+ }
4367
4397
  if (['thread/start', 'thread/resume', 'thread/fork'].includes(message.method)) {
4368
4398
  const threadId =
4369
4399
  isRecord(message.params) && typeof message.params.threadId === 'string'
@@ -4428,7 +4458,7 @@ export function createCodexContextProxy({
4428
4458
  let passthrough = false;
4429
4459
  let discardOversized = false;
4430
4460
  let pausedForBackpressure = false;
4431
- const forward = (text) => {
4461
+ const writeOut = (text) => {
4432
4462
  if (!stdout.write(text) && !pausedForBackpressure) {
4433
4463
  pausedForBackpressure = true;
4434
4464
  child.stdout.pause();
@@ -4438,6 +4468,54 @@ export function createCodexContextProxy({
4438
4468
  });
4439
4469
  }
4440
4470
  };
4471
+ const forward = (text) => {
4472
+ if (engineeringHeld === null) {
4473
+ writeOut(text);
4474
+ return;
4475
+ }
4476
+ engineeringHeld.lines.push(text);
4477
+ engineeringHeld.bytes += Buffer.byteLength(text, 'utf8');
4478
+ if (engineeringHeld.bytes > MAX_LINE_BYTES) releaseEngineering();
4479
+ };
4480
+ /** The client's first successful initialize answer, which starts the hold. */
4481
+ function isEngineeringTrigger(message) {
4482
+ if (engineeringState !== 'waiting' || !isRecord(message)) return false;
4483
+ if (message.method !== undefined || !('result' in message)) return false;
4484
+ const key = clientRequestKey(message.id);
4485
+ return key !== null && initializeRequestKeys.has(key);
4486
+ }
4487
+ function holdForEngineering(text) {
4488
+ engineeringState = 'holding';
4489
+ initializeRequestKeys.clear();
4490
+ engineeringHeld = {
4491
+ lines: [text],
4492
+ bytes: Buffer.byteLength(text, 'utf8'),
4493
+ id: null,
4494
+ cancel: null,
4495
+ };
4496
+ try {
4497
+ engineeringHeld.id = sendOwned(
4498
+ 'skills/extraRoots/set',
4499
+ { extraRoots: [engineeringSkillsRoot] },
4500
+ 'engineering',
4501
+ null,
4502
+ );
4503
+ } catch {
4504
+ releaseEngineering();
4505
+ return;
4506
+ }
4507
+ engineeringHeld.cancel = scheduleTimeout(() => releaseEngineering(), engineeringAckTimeoutMs);
4508
+ }
4509
+ /** Forward everything held, in order. Idempotent; the session never waits on the pack. */
4510
+ function releaseEngineering() {
4511
+ if (engineeringHeld === null) return;
4512
+ const held = engineeringHeld;
4513
+ engineeringHeld = null;
4514
+ engineeringState = 'done';
4515
+ held.cancel?.();
4516
+ if (held.id !== null && ownedRequests.has(held.id)) cleanupRequest(held.id);
4517
+ for (const text of held.lines) writeOut(text);
4518
+ }
4441
4519
  const processLine = (line) => {
4442
4520
  if (Buffer.byteLength(line, 'utf8') + 1 > MAX_LINE_BYTES) {
4443
4521
  const ownedId = internalRequestId(line);
@@ -4451,6 +4529,10 @@ export function createCodexContextProxy({
4451
4529
  try {
4452
4530
  const message = JSON.parse(line.endsWith('\r') ? line.slice(0, -1) : line);
4453
4531
  observeClientResponse(message);
4532
+ if (isEngineeringTrigger(message)) {
4533
+ holdForEngineering(`${line}\n`);
4534
+ return;
4535
+ }
4454
4536
  forwardLine = handle(message);
4455
4537
  } catch {
4456
4538
  /* preserve malformed external protocol lines */
@@ -4536,6 +4618,7 @@ export function createCodexContextProxy({
4536
4618
  }
4537
4619
  });
4538
4620
  child.stdout.on('end', () => {
4621
+ releaseEngineering();
4539
4622
  const tail = inputBuffer + decoder.end();
4540
4623
  if (tail && !discardOversized) forward(tail);
4541
4624
  inputBuffer = '';
@@ -4565,6 +4648,7 @@ export function createCodexContextProxy({
4565
4648
  observeClientRequest,
4566
4649
  observeClientResponse,
4567
4650
  async stop() {
4651
+ releaseEngineering();
4568
4652
  if (stopped) {
4569
4653
  await Promise.all(
4570
4654
  [...diagnosticSinks.values()].map((sink) =>
@@ -4594,6 +4678,18 @@ export function createCodexContextProxy({
4594
4678
  };
4595
4679
  }
4596
4680
 
4681
+ /**
4682
+ * The launcher's AISAC Engineering skills root, or null. Only an absolute,
4683
+ * normalized path without control characters is used.
4684
+ */
4685
+ export function engineeringSkillsRootFrom(value) {
4686
+ if (typeof value !== 'string' || value.length === 0) return null;
4687
+ if (value.length > MAX_ENGINEERING_PATH_LENGTH || /[\u0000-\u001f\u007f]/u.test(value))
4688
+ return null;
4689
+ if (!value.startsWith('/') || resolve(value) !== value) return null;
4690
+ return value;
4691
+ }
4692
+
4597
4693
  async function main() {
4598
4694
  if (process.argv.includes('--help')) {
4599
4695
  process.stderr.write(
@@ -4648,6 +4744,7 @@ async function main() {
4648
4744
  timeoutMs: numberEnv('CODEX_CONTEXT_COMPACT_TIMEOUT_MS', DEFAULT_TIMEOUT_MS),
4649
4745
  policy,
4650
4746
  createDiagnosticSink: diagnosticFactory,
4747
+ engineeringSkillsRoot: engineeringSkillsRootFrom(process.env[ENGINEERING_SKILLS_ROOT_ENV]),
4651
4748
  });
4652
4749
  const clientDecoder = new StringDecoder('utf8');
4653
4750
  let clientInputBuffer = '';
@@ -16,7 +16,7 @@ export const MAX_LOG_FILES = 3;
16
16
  const LOCK_WAIT_MS = 250;
17
17
  const LOCK_STALE_MS = 30_000;
18
18
  const ADMISSION_MS = 60 * 60 * 1000;
19
- export const ADAPTER_VERSION = '0.4.0-beta.1';
19
+ export const ADAPTER_VERSION = '0.4.0-beta.3';
20
20
  const MAX_DIAGNOSTIC_COUNT = 10_000_000;
21
21
  const MAX_DIAGNOSTIC_BYTES = 128 * 1024 * 1024;
22
22
  const CHECKPOINT_BACKENDS = new Set(['native_store', 'private_file']);
package/dist/cli.mjs CHANGED
@@ -29,8 +29,15 @@ import { codexPolicyJson, makePolicy, publicPolicy, isProfile } from './policy.m
29
29
  import { readDiagnostics } from './claude-observation.mjs';
30
30
  import { readCodexDiagnostics } from './codex-diagnostics.mjs';
31
31
  import { checkEngineeringLicense, licenseLine, unavailableLicense } from './engineering.mjs';
32
+ import {
33
+ isPackLocale,
34
+ readLastLaunch,
35
+ readStoredLocale,
36
+ resolvePackLocale,
37
+ writeStoredLocale,
38
+ } from './engineering-pack.mjs';
32
39
 
33
- const VERSION = '0.4.0-beta.1';
40
+ const VERSION = '0.4.0-beta.3';
34
41
  // The command is `aisac`; `aibrain` is an alias installed from the same entry.
35
42
  const COMMAND = 'aisac';
36
43
  const ENDPOINT = 'https://mcp.ai-brain.aisac.cloud/mcp';
@@ -48,7 +55,7 @@ function usage() {
48
55
  'Claude shadow: setup --claude-shadow on|off and --claude-diagnostics on|off (preview stays applied=false).',
49
56
  );
50
57
  console.log(
51
- 'AISAC Engineering: setup and doctor report the license; status stays offline. `aibrain` is an alias of `aisac`.',
58
+ 'AISAC Engineering: `aisac claude` and `aisac codex` (Codex 0.164+) load the licensed method; setup --lang pt-BR|en picks its language; setup and doctor report the license; status stays offline. `aibrain` is an alias of `aisac`.',
52
59
  );
53
60
  }
54
61
 
@@ -310,6 +317,26 @@ async function updateLocalOptOut(previous, args) {
310
317
  return true;
311
318
  }
312
319
 
320
+ /** Store the AISAC Engineering pack language; local only, no network call. @param {'pt-BR' | 'en'} locale */
321
+ async function setPackLocale(locale) {
322
+ const root = await ensureStateRoot();
323
+ await writeStoredLocale(root, locale);
324
+ console.log(
325
+ `AISAC Engineering pack language set to ${locale} for future launches; no network call was made.`,
326
+ );
327
+ }
328
+
329
+ /** Offline view of the pack language and the last launch (status, doctor). */
330
+ async function engineeringPackState() {
331
+ const root = stateRoot();
332
+ const stored = await readStoredLocale(root);
333
+ return {
334
+ locale: resolvePackLocale(stored),
335
+ localeSource: stored === null ? 'system' : 'setup',
336
+ lastLaunch: await readLastLaunch(root),
337
+ };
338
+ }
339
+
313
340
  /** @param {string} endpoint */
314
341
  function validateEndpoint(endpoint) {
315
342
  if (endpoint.length > 2048) throw new Error('endpoint_too_long');
@@ -385,6 +412,7 @@ async function setup(args) {
385
412
  '--telemetry',
386
413
  '--claude-shadow',
387
414
  '--claude-diagnostics',
415
+ '--lang',
388
416
  ]),
389
417
  new Set([
390
418
  '--client',
@@ -396,10 +424,14 @@ async function setup(args) {
396
424
  '--telemetry',
397
425
  '--claude-shadow',
398
426
  '--claude-diagnostics',
427
+ '--lang',
399
428
  ]),
400
429
  );
401
430
  if (args.includes('--undo') && args.length !== 1)
402
431
  throw new Error('setup_undo_cannot_have_options');
432
+ const lang = option(args, '--lang');
433
+ if (lang !== undefined && !isPackLocale(lang)) throw new Error('invalid_lang');
434
+ if (lang !== undefined && args.length === 2) return setPackLocale(lang);
403
435
  const previous = await readConfig();
404
436
  if (await updateLocalOptOut(previous, args)) return;
405
437
  const localOptOut =
@@ -501,6 +533,7 @@ async function setup(args) {
501
533
  console.log(
502
534
  `Configured ${clients} in ${root}. Provider-free access verified; no provider batch was used.`,
503
535
  );
536
+ if (lang !== undefined) await setPackLocale(lang);
504
537
  // Display only: the server decides again on every pack request (ADR-0047).
505
538
  const line = licenseLine(await checkEngineeringLicense(endpoint, token));
506
539
  if (line !== null) console.log(line);
@@ -599,6 +632,7 @@ async function doctor(json) {
599
632
  config?.claudeObservation?.disclosedAt !== undefined,
600
633
  },
601
634
  engineering,
635
+ engineeringPack: await engineeringPackState(),
602
636
  paidOrPilot: probe?.ok
603
637
  ? 'provider_free_probe_ok; paid entitlement or pilot invitation remains server-enforced'
604
638
  : config
@@ -634,6 +668,7 @@ async function status(json) {
634
668
  config?.claudeObservation?.disclosedAt !== null &&
635
669
  config?.claudeObservation?.disclosedAt !== undefined,
636
670
  },
671
+ engineeringPack: await engineeringPackState(),
637
672
  monitoredSession: null,
638
673
  monitoringStatus: 'not_observed',
639
674
  };
@@ -0,0 +1,165 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * AISAC Engineering for Codex (ADR-0047 wave E4c, spec FR4).
5
+ *
6
+ * Codex has no session plugin directory, so the method reaches only the
7
+ * app-server this launch owns, and nothing is written to `~/.codex`,
8
+ * `~/.agents` or the project:
9
+ * - skills: the adapter sends `skills/extraRoots/set` with the pack's
10
+ * `skills/` directory right after the client's initialize (in-memory in that
11
+ * process);
12
+ * - agents: one role file per pack agent, declared with `-c agents.<name>.*`
13
+ * overrides on the owned app-server (the session-flags config layer).
14
+ * The launcher hands the monitor one JSON value; the monitor validates it
15
+ * again before turning it into arguments.
16
+ */
17
+
18
+ import { basename, dirname, join, resolve } from 'node:path';
19
+
20
+ export const CODEX_ENGINEERING_ENV = 'AISAC_ENGINEERING_CODEX';
21
+ export const CODEX_ENGINEERING_SKILLS_ROOT_ENV = 'AISAC_ENGINEERING_SKILLS_ROOT';
22
+ export const CODEX_AGENTS_DIRECTORY = 'codex-agents';
23
+ const SKILLS_DIRECTORY = 'skills';
24
+ const PACK_DIRECTORY = 'engineering';
25
+ const RUNTIME_DIRECTORY = 'runtime';
26
+ const CODEX_LAUNCH_DIRECTORY = /^codex-[A-Za-z0-9]{6}$/u;
27
+ const SCHEMA_VERSION = 1;
28
+ const MAX_SPEC_BYTES = 16 * 1024;
29
+ const MAX_AGENTS = 16;
30
+ const MAX_PATH_LENGTH = 4096;
31
+ export const MAX_AGENT_DESCRIPTION_LENGTH = 1024;
32
+ export const AGENT_NAME = /^[a-z][a-z0-9-]{0,63}$/u;
33
+ const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/u;
34
+ /** First Codex release whose session mechanism was verified (spike Q4, 2026-11-03). */
35
+ const MIN_CODEX_VERSION = /** @type {const} */ ([0, 164, 0]);
36
+
37
+ /** @typedef {{ name: string, description: string, configFile: string }} CodexAgentRole */
38
+ /** @typedef {{ schemaVersion: 1, skillsRoot: string, agents: CodexAgentRole[] }} CodexEngineering */
39
+
40
+ /** @param {string} code @returns {never} */
41
+ function fail(code) {
42
+ throw new Error(`codex_engineering_${code}`);
43
+ }
44
+
45
+ /** @param {unknown} value @returns {value is Record<string, unknown>} */
46
+ function isRecord(value) {
47
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
48
+ }
49
+
50
+ /**
51
+ * Whether this Codex version receives the pack. A null or malformed version
52
+ * never does.
53
+ * @param {string | null | undefined} version
54
+ */
55
+ export function supportsCodexEngineering(version) {
56
+ const match = /^(\d+)\.(\d+)\.(\d+)$/u.exec(version ?? '');
57
+ if (match === null) return false;
58
+ const parts = match.slice(1).map(Number);
59
+ for (let index = 0; index < MIN_CODEX_VERSION.length; index += 1) {
60
+ const part = /** @type {number} */ (parts[index]);
61
+ const minimum = MIN_CODEX_VERSION[index] ?? 0;
62
+ if (part !== minimum) return part > minimum;
63
+ }
64
+ return true;
65
+ }
66
+
67
+ /** @param {unknown} value */
68
+ function isCleanAbsolutePath(value) {
69
+ return (
70
+ typeof value === 'string' &&
71
+ value.length > 0 &&
72
+ value.length <= MAX_PATH_LENGTH &&
73
+ !CONTROL_CHARACTERS.test(value) &&
74
+ value.startsWith('/') &&
75
+ resolve(value) === value
76
+ );
77
+ }
78
+
79
+ /**
80
+ * @param {unknown} value
81
+ * @param {{ stateDir?: string }} [options] when given, the pack must sit in a
82
+ * Codex launch directory of that state directory
83
+ * @returns {CodexEngineering}
84
+ */
85
+ export function validateCodexEngineering(value, options = {}) {
86
+ if (!isRecord(value)) fail('invalid');
87
+ const keys = Object.keys(value).sort().join(',');
88
+ if (keys !== 'agents,schemaVersion,skillsRoot') fail('invalid');
89
+ if (value.schemaVersion !== SCHEMA_VERSION) fail('schema');
90
+ const skillsRoot = value.skillsRoot;
91
+ if (!isCleanAbsolutePath(skillsRoot)) fail('path');
92
+ const skills = /** @type {string} */ (skillsRoot);
93
+ if (basename(skills) !== SKILLS_DIRECTORY) fail('path');
94
+ const base = dirname(skills);
95
+ if (basename(base) !== PACK_DIRECTORY) fail('path');
96
+ if (options.stateDir !== undefined) {
97
+ const launch = dirname(base);
98
+ if (
99
+ !CODEX_LAUNCH_DIRECTORY.test(basename(launch)) ||
100
+ dirname(launch) !== join(resolve(options.stateDir), RUNTIME_DIRECTORY)
101
+ )
102
+ fail('path');
103
+ }
104
+ if (!Array.isArray(value.agents) || value.agents.length > MAX_AGENTS) fail('agents');
105
+ const seen = new Set();
106
+ /** @type {CodexAgentRole[]} */
107
+ const agents = [];
108
+ for (const agent of value.agents) {
109
+ if (!isRecord(agent) || Object.keys(agent).sort().join(',') !== 'configFile,description,name')
110
+ fail('agents');
111
+ const { name, description, configFile } = agent;
112
+ if (typeof name !== 'string' || !AGENT_NAME.test(name) || seen.has(name)) fail('agents');
113
+ if (
114
+ typeof description !== 'string' ||
115
+ description.trim().length === 0 ||
116
+ description.length > MAX_AGENT_DESCRIPTION_LENGTH ||
117
+ CONTROL_CHARACTERS.test(description)
118
+ )
119
+ fail('agents');
120
+ if (configFile !== join(base, CODEX_AGENTS_DIRECTORY, `${name}.toml`)) fail('agents');
121
+ seen.add(name);
122
+ agents.push({ name, description, configFile });
123
+ }
124
+ return { schemaVersion: SCHEMA_VERSION, skillsRoot: skills, agents };
125
+ }
126
+
127
+ /**
128
+ * Parse the monitor's environment value; null when absent.
129
+ * @param {string | undefined} raw
130
+ * @param {{ stateDir?: string }} [options]
131
+ * @returns {CodexEngineering | null}
132
+ */
133
+ export function parseCodexEngineering(raw, options = {}) {
134
+ if (raw === undefined || raw === '') return null;
135
+ if (Buffer.byteLength(raw, 'utf8') > MAX_SPEC_BYTES) fail('too_large');
136
+ let value;
137
+ try {
138
+ value = JSON.parse(raw);
139
+ } catch {
140
+ fail('invalid');
141
+ }
142
+ return validateCodexEngineering(value, options);
143
+ }
144
+
145
+ /**
146
+ * A TOML basic string. JSON escapes are TOML escapes; DEL is escaped too.
147
+ * @param {string} value
148
+ */
149
+ export function tomlBasicString(value) {
150
+ return JSON.stringify(value).replace(/\u007f/gu, '\\u007F');
151
+ }
152
+
153
+ /**
154
+ * Session-only `-c` overrides for the owned app-server, one role per agent.
155
+ * @param {CodexEngineering} engineering
156
+ * @returns {string[]}
157
+ */
158
+ export function codexEngineeringConfigArgs(engineering) {
159
+ return engineering.agents.flatMap((agent) => [
160
+ '-c',
161
+ `agents.${agent.name}.description=${tomlBasicString(agent.description)}`,
162
+ '-c',
163
+ `agents.${agent.name}.config_file=${tomlBasicString(agent.configFile)}`,
164
+ ]);
165
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "schema_version": 1,
3
+ "keys": [
4
+ {
5
+ "key_id": "c197814f9e4884ac",
6
+ "x": "Oict0CKYWwgmFovl3TDPJUJ3C4do9Nu6xXU6m4PPOQc",
7
+ "created": "2026-11-02"
8
+ }
9
+ ]
10
+ }