@volter/supercode-interchange 0.1.22 → 0.1.24

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 (129) hide show
  1. package/LICENSE-diffy-MIT +23 -0
  2. package/README.md +422 -2
  3. package/channel-records.mjs +71 -0
  4. package/channel-session-counts.mjs +81 -0
  5. package/channels-read.mjs +90 -0
  6. package/corpus-audit-records.mjs +192 -0
  7. package/corpus-audit-render.mjs +47 -0
  8. package/corpus-audit-schema.mjs +89 -0
  9. package/corpus-audit.d.ts +16 -0
  10. package/corpus-audit.mjs +132 -0
  11. package/file-io.mjs +21 -2
  12. package/harness-settings.mjs +19 -0
  13. package/hermes-fire-links.mjs +135 -0
  14. package/hermes-fires.mjs +106 -0
  15. package/hermes-read-home.d.ts +3 -0
  16. package/hermes-read-home.mjs +235 -0
  17. package/hermes-read-store.mjs +49 -0
  18. package/hermes-read-validation.mjs +170 -0
  19. package/hermes-read-workflow.mjs +164 -0
  20. package/hermes-read-yaml.mjs +68 -0
  21. package/hermes-store.mjs +1 -1
  22. package/homes.mjs +2 -2
  23. package/index.d.ts +14 -1
  24. package/index.mjs +1 -1
  25. package/inspection-builtin-tools.json +261 -0
  26. package/inspection-config-fields.json +765 -0
  27. package/inspection-schemas.d.ts +4 -0
  28. package/inspection-schemas.mjs +117 -0
  29. package/job-notepad-read.mjs +97 -0
  30. package/jobs-control.mjs +250 -0
  31. package/jobs-read.mjs +198 -0
  32. package/jsonl.mjs +4 -3
  33. package/memory-show.mjs +187 -0
  34. package/metadata.mjs +45 -3
  35. package/native-json.mjs +46 -3
  36. package/ontology-contracts.d.ts +5 -0
  37. package/ontology-contracts.mjs +29 -0
  38. package/ontology-operations.json +198 -0
  39. package/ontology-schemas.json +7946 -0
  40. package/openclaw-jobs-read.mjs +117 -0
  41. package/openclaw-profile-home.mjs +142 -0
  42. package/orchestrator-read-records.mjs +112 -0
  43. package/orchestrator-store.mjs +21 -9
  44. package/package.json +59 -4
  45. package/parity-ledger.json +6984 -0
  46. package/parity-orchestration.json +5538 -0
  47. package/parity-report.d.ts +7 -0
  48. package/parity-report.mjs +46 -0
  49. package/preset-profiles.mjs +27 -0
  50. package/profile-command.mjs +32 -0
  51. package/profile-orchestrator-door.mjs +104 -0
  52. package/profile-paths.mjs +25 -0
  53. package/profiles-control.mjs +98 -0
  54. package/profiles-read.mjs +119 -0
  55. package/reduction-attribution.mjs +54 -0
  56. package/reduction-diff.mjs +165 -0
  57. package/reduction-log.mjs +177 -0
  58. package/reduction-view.mjs +145 -0
  59. package/retained-projection.mjs +9 -0
  60. package/routes-read.mjs +103 -0
  61. package/runs-read.mjs +224 -0
  62. package/session-activity.d.ts +23 -0
  63. package/session-activity.mjs +124 -0
  64. package/session-append-scan.mjs +27 -17
  65. package/session-branch-handoff.mjs +115 -0
  66. package/session-claude-graph.mjs +29 -7
  67. package/session-claude-index.mjs +732 -114
  68. package/session-claude-runtime.d.ts +28 -0
  69. package/session-claude-runtime.mjs +171 -6
  70. package/session-claude-scan-store.mjs +164 -0
  71. package/session-claude.mjs +9 -0
  72. package/session-codex-activity.d.ts +6 -0
  73. package/session-codex-activity.mjs +120 -0
  74. package/session-codex-window.mjs +24 -4
  75. package/session-config-scan.mjs +141 -0
  76. package/session-convert.d.ts +33 -0
  77. package/session-convert.mjs +179 -0
  78. package/session-emission-version.json +1 -0
  79. package/session-file.mjs +11 -1
  80. package/session-follower.mjs +17 -5
  81. package/session-fresh-id.mjs +15 -0
  82. package/session-hermes-import.mjs +61 -0
  83. package/session-hermes.mjs +1 -1
  84. package/session-import.mjs +37 -0
  85. package/session-inspect.d.ts +18 -0
  86. package/session-inspect.mjs +122 -0
  87. package/session-machine-tree.d.ts +33 -0
  88. package/session-machine-tree.mjs +274 -0
  89. package/session-materialize-files.mjs +78 -0
  90. package/session-materialize-opencode.mjs +137 -0
  91. package/session-materialize.mjs +186 -0
  92. package/session-message.mjs +85 -0
  93. package/session-model.d.ts +9 -0
  94. package/session-model.mjs +1 -0
  95. package/session-opencode-store.mjs +39 -3
  96. package/session-path.mjs +6 -3
  97. package/session-pi-content.mjs +7 -2
  98. package/session-read.d.ts +134 -2
  99. package/session-read.mjs +86 -9
  100. package/session-recorded-config.mjs +3 -2
  101. package/session-records.mjs +12 -6
  102. package/session-residue-store.mjs +90 -0
  103. package/session-residue.mjs +150 -0
  104. package/session-resume-instructions.mjs +1 -1
  105. package/session-saved-read.mjs +105 -0
  106. package/session-transfer.mjs +141 -0
  107. package/session-views.mjs +1 -0
  108. package/session-write-claude.mjs +135 -0
  109. package/session-write-codex.mjs +146 -0
  110. package/session-write-gemini.mjs +77 -0
  111. package/session-write-goose.mjs +99 -0
  112. package/session-write-grok.mjs +70 -0
  113. package/session-write-native.mjs +41 -0
  114. package/session-write-opencode.mjs +197 -0
  115. package/session-write-pi.mjs +130 -0
  116. package/session-write-residue.mjs +19 -0
  117. package/session-write-utils.mjs +77 -0
  118. package/session-write.d.ts +10 -0
  119. package/session-write.mjs +37 -0
  120. package/skill-toml.mjs +80 -0
  121. package/skills-control.mjs +193 -0
  122. package/skills-list.mjs +237 -0
  123. package/source-files.mjs +12 -2
  124. package/sqlite-reader.mjs +134 -18
  125. package/support-registry.json +402 -0
  126. package/support-report.mjs +69 -0
  127. package/trigger-records.mjs +77 -0
  128. package/triggers-read.mjs +67 -0
  129. package/workflow-read.mjs +165 -0
@@ -0,0 +1,19 @@
1
+ // User-level interoperation evidence. Configuration writes and effective
2
+ // process policy retain their own owners.
3
+ import { resolveHarnessHomes } from './homes.mjs';
4
+ import { readClaudeInteropSettings } from './session-message.mjs';
5
+
6
+ const invalid = message => { throw Object.assign(new Error(message), { rpcCode: -32602 }); };
7
+ export function loadHarnessSettings(params, { env = process.env, cwd = process.cwd() } = {}) {
8
+ if (params == null || typeof params !== 'object') invalid('settings params must be a struct');
9
+ if (Array.isArray(params)) {
10
+ if (params.length !== 1) invalid('settings params require exactly one harness');
11
+ params = { harness: params[0] };
12
+ } else if (Object.keys(params).some(key => key !== 'harness')) invalid('unknown settings parameter');
13
+ if (typeof params.harness !== 'string') invalid('harness must be a string');
14
+ if (params.harness !== 'claude-code') return {
15
+ schema: 'supercode.harness-interop-settings.v1', harness: params.harness,
16
+ revision: '', controls: [], advisories: [],
17
+ };
18
+ return readClaudeInteropSettings(resolveHarnessHomes(env), cwd);
19
+ }
@@ -0,0 +1,135 @@
1
+ // Derived fire/session/obligation links, without writing the source ledgers.
2
+ import { parseNativeJson } from './native-json.mjs';
3
+ import { hermesFireSession, hermesRowText } from './hermes-fires.mjs';
4
+ import { hermesObject, hermesRefuse } from './hermes-read-validation.mjs';
5
+
6
+ const compare = (left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right));
7
+ const required = (file, key, value) => typeof value === 'string' && value !== '' ? value : hermesRefuse(file, key, 'expected a non-empty string');
8
+
9
+ export function hermesSessionProfile(key) {
10
+ if (typeof key !== 'string') return null;
11
+ const parts = key.split(':');
12
+ return parts[0] === 'agent' && parts.length >= 4 && !['', 'main', 'default'].includes(parts[1]) ? parts[1] : null;
13
+ }
14
+
15
+ // serde's tagged Fire source refuses duplicate modeled fields. The ordinary
16
+ // native Value decoder otherwise keeps the last occurrence of an object key.
17
+ function sourceFire(text) {
18
+ if (text == null) return null;
19
+ let parsed; try { parsed = parseNativeJson(text); } catch { return null; }
20
+ if (!hermesObject(parsed) || parsed.kind !== 'fire' || typeof parsed.fire_id !== 'string') return null;
21
+ const counts = new Map(); let depth = 0;
22
+ for (let at = 0; at < text.length; at++) {
23
+ const char = text[at];
24
+ if (char === '{' || char === '[') { depth++; continue; }
25
+ if (char === '}' || char === ']') { depth--; continue; }
26
+ if (char !== '"') continue;
27
+ const start = at++;
28
+ for (; at < text.length; at++) { if (text[at] === '\\') at++; else if (text[at] === '"') break; }
29
+ if (depth !== 1) continue;
30
+ let after = at + 1; while (/[\x20\t\r\n]/u.test(text[after] ?? '') && after < text.length) after++;
31
+ if (text[after] !== ':') continue;
32
+ const key = JSON.parse(text.slice(start, at + 1)); counts.set(key, (counts.get(key) ?? 0) + 1);
33
+ }
34
+ return (counts.get('kind') ?? 0) > 1 || (counts.get('fire_id') ?? 0) > 1 ? null : parsed.fire_id;
35
+ }
36
+
37
+ export function decodeHermesObligation(file, row) {
38
+ const id = required(file, 'obligation_id', row.obligation_id);
39
+ if (!['pending', 'delivered', 'sent', 'failed', 'dropped'].includes(row.state)) hermesRefuse(file, `${id}.state`, `unknown obligation state ${JSON.stringify(typeof row.state === 'string' ? row.state : '')}`);
40
+ const session_key = typeof row.session_key === 'string' && row.session_key !== '' ? row.session_key : null;
41
+ return { id, session_key, target: { platform: required(file, `${id}.platform`, row.platform), chat_id: hermesRowText(row, 'chat_id') ?? '' },
42
+ // Kept inside the complete reader for projections of this same obligation.
43
+ row,
44
+ created_at: hermesRowText(row, 'created_at') ?? 'undefined',
45
+ source_fire: sourceFire(hermesRowText(row, 'source_json')) };
46
+ }
47
+
48
+ const signed = value => {
49
+ if (!/^[+-]?[0-9]+$/u.test(value)) return null;
50
+ const whole = BigInt(value);
51
+ return whole >= -(1n << 63n) && whole <= (1n << 63n) - 1n ? whole : null;
52
+ };
53
+ const float = value => {
54
+ if (typeof value !== 'string') return null;
55
+ if (/^[+-]?(?:inf|infinity)$/iu.test(value)) return value.startsWith('-') ? -Infinity : Infinity;
56
+ if (/^[+-]?nan$/iu.test(value)) return NaN;
57
+ return /^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/u.test(value) ? Number(value) : null;
58
+ };
59
+ const wrap = value => BigInt.asIntN(64, value);
60
+
61
+ /** Native ISO epoch arithmetic, including its signed component parser and
62
+ * float seconds; this is not Date's calendar normalization. */
63
+ export function hermesIsoEpoch(iso) {
64
+ if (typeof iso !== 'string') return null;
65
+ let instant, offset;
66
+ if (iso.endsWith('Z')) { instant = iso.slice(0, -1); offset = 0; }
67
+ else {
68
+ const time = iso.indexOf('T'); if (time < 0) return null;
69
+ const sign = iso.slice(time).search(/[+-]/u); if (sign < 0) return null;
70
+ const at = time + sign, raw = iso.slice(at), colon = raw.indexOf(':'); if (colon < 0) return null;
71
+ const hours = float(raw.slice(1, colon)), minutes = float(raw.slice(colon + 1));
72
+ if (hours == null || minutes == null) return null;
73
+ offset = (hours * 3600 + minutes * 60) * (raw.startsWith('-') ? -1 : 1); instant = iso.slice(0, at);
74
+ }
75
+ const split = instant.indexOf('T'); if (split < 0) return null;
76
+ const date = instant.slice(0, split), time = instant.slice(split + 1);
77
+ const firstDate = date.indexOf('-'), secondDate = date.indexOf('-', firstDate + 1);
78
+ const firstTime = time.indexOf(':'), secondTime = time.indexOf(':', firstTime + 1);
79
+ if ([firstDate, secondDate, firstTime, secondTime].some(at => at < 0)) return null;
80
+ let year = signed(date.slice(0, firstDate));
81
+ const month = signed(date.slice(firstDate + 1, secondDate)), day = signed(date.slice(secondDate + 1));
82
+ const hour = signed(time.slice(0, firstTime)), minute = signed(time.slice(firstTime + 1, secondTime)), seconds = float(time.slice(secondTime + 1));
83
+ if ([year, month, day, hour, minute, seconds].some(value => value == null)) return null;
84
+ year = wrap(year - (month <= 2n ? 1n : 0n));
85
+ const era = year < 0n ? (year - 399n) / 400n : year / 400n, yoe = wrap(year - wrap(era * 400n));
86
+ const adj = wrap(month > 2n ? month - 3n : month + 9n);
87
+ const doy = wrap(wrap(wrap(153n * adj) + 2n) / 5n + day - 1n);
88
+ const doe = wrap(wrap(wrap(yoe * 365n) + yoe / 4n) - yoe / 100n + doy);
89
+ const days = wrap(wrap(era * 146097n) + doe - 719468n);
90
+ return Number(wrap(wrap(wrap(days * 86400n) + wrap(hour * 3600n)) + wrap(minute * 60n))) + seconds - offset;
91
+ }
92
+
93
+ function jobSurface(job) {
94
+ if (job?.deliver.kind === 'origin' && job.origin?.chat_id != null) return [job.origin.platform, job.origin.chat_id];
95
+ if (job?.deliver.kind === 'explicit' && job.deliver.chat_id != null) return [job.deliver.platform, job.deliver.chat_id];
96
+ return null;
97
+ }
98
+
99
+ /** Each profile is linked as a batch, then its derived obligation sources
100
+ * become visible to the next profile in native profile-key order. */
101
+ export function linkHermesFires(profiles, sessionsFor) {
102
+ const names = [...profiles.keys()].sort(compare), lenders = profiles.get('default').lenders;
103
+ for (const name of names) {
104
+ const profile = profiles.get(name), sessions = sessionsFor(profile);
105
+ const holders = profile.borrowed || profile.lenders.length ? ['default', ...lenders] : [name];
106
+ const known = new Map();
107
+ for (const holder of names) for (const obligation of profiles.get(holder).obligations) {
108
+ if (obligation.source_fire != null && !known.has(obligation.source_fire)) known.set(obligation.source_fire, [holder, obligation.id]);
109
+ }
110
+ const links = profile.fires.map(fire => {
111
+ if (known.has(fire.id)) return [fire.session_id, known.get(fire.id)];
112
+ const session = hermesFireSession(sessions, fire) ?? fire.session_id;
113
+ const rawKey = session == null ? null : sessions.find(row => row.id === session)?.session_key;
114
+ const key = typeof rawKey === 'string' && rawKey !== '' ? rawKey : null;
115
+ const surface = jobSurface(profile.jobs.get(fire.job_id)), from = hermesIsoEpoch(fire.claimed_at);
116
+ const to = hermesIsoEpoch(fire.finished_at) ?? Number.MAX_VALUE;
117
+ if (from == null) return [session, null];
118
+ const candidates = holders.flatMap(holder => profiles.get(holder).obligations.flatMap(obligation => {
119
+ const at = float(obligation.created_at);
120
+ return at != null && at >= from && at <= to ? [{ holder, obligation, at }] : [];
121
+ }));
122
+ const latest = predicate => candidates.filter(predicate).sort((left, right) => right.at - left.at)[0];
123
+ const selected = (key == null ? null : latest(row => row.obligation.session_key === key))
124
+ ?? (surface == null ? null : latest(row => row.obligation.target.platform === surface[0] && row.obligation.target.chat_id === surface[1]));
125
+ return [session, selected == null ? null : [selected.holder, selected.obligation.id]];
126
+ });
127
+ links.forEach(([session, obligation], index) => {
128
+ const fire = profile.fires[index]; fire.session_id = session; fire.obligation_id = obligation?.[1] ?? null;
129
+ if (obligation != null) {
130
+ const linked = profiles.get(obligation[0]).obligations.find(row => row.id === obligation[1]);
131
+ if (linked) linked.source_fire = fire.id;
132
+ }
133
+ });
134
+ }
135
+ }
@@ -0,0 +1,106 @@
1
+ // Fire decoding and session linking used by the complete Hermes home reader.
2
+ // This module reads no files and does not replace the home's validation step.
3
+ import { nativeInteger, nativeNumberKind, parseNativeJson, stringifyNativeJsonMember } from './native-json.mjs';
4
+ import { nativeTrim } from './text.mjs';
5
+
6
+ const object = value => value != null && typeof value === 'object' && !Array.isArray(value);
7
+ const compare = (left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right));
8
+ const statuses = { claimed: 'claimed', running: 'running', completed: 'succeeded', failed: 'failed', unknown: 'unknown' };
9
+ const mapped = new Set(['id', 'job_id', 'status', 'claimed_at', 'started_at', 'finished_at', 'error', 'residue_json', 'session_id', 'obligation_id']);
10
+ const refuse = (file, key, message) => { throw new Error(`${file}${key ? ` [${key}]` : ''}: ${message}`); };
11
+ const required = (file, key, value) => typeof value === 'string' && value !== '' ? value : refuse(file, key, 'expected a non-empty string');
12
+ const optional = (file, key, value) => value == null ? null : typeof value === 'string' ? value : refuse(file, key, 'expected a string');
13
+
14
+ /** SQLite rows have already been converted to native JSON values by the
15
+ * read-only store boundary. Integral REAL values remain floats there. */
16
+ export function hermesRowText(row, key) {
17
+ const value = row[key];
18
+ if (typeof value === 'string') return value;
19
+ if (typeof value !== 'number') return null;
20
+ if (nativeNumberKind(row, key) === 'float' && Number.isInteger(value) && Math.abs(value) < 1e21) {
21
+ const whole = BigInt(value), low = -(1n << 63n), high = (1n << 63n) - 1n;
22
+ return String(whole < low ? low : whole > high ? high : whole);
23
+ }
24
+ return stringifyNativeJsonMember(row, key);
25
+ }
26
+
27
+ export function decodeHermesFire(file, row) {
28
+ const id = required(file, 'id', row.id), word = typeof row.status === 'string' ? row.status : '';
29
+ if (!Object.hasOwn(statuses, word)) refuse(file, `${id}.status`, `unknown fire status ${JSON.stringify(word)}`);
30
+ const members = new Map();
31
+ for (const key of Object.keys(row)) if (!mapped.has(key) && row[key] != null) members.set(key, stringifyNativeJsonMember(row, key));
32
+ const extra = hermesRowText(row, 'residue_json');
33
+ if (extra != null) {
34
+ let parsed; try { parsed = parseNativeJson(extra); } catch { /* malformed extras are ignored by the native decoder */ }
35
+ if (object(parsed)) for (const key of Object.keys(parsed)) members.set(key, stringifyNativeJsonMember(parsed, key));
36
+ }
37
+ // Reconstruct all member facts together: an extra string/null replacing an
38
+ // integer must remove that field's previous integer interpretation too.
39
+ const residue = parseNativeJson(`{${[...members].map(([key, value]) => `${JSON.stringify(key)}:${value}`).join(',')}}`);
40
+ return { id, job_id: required(file, `${id}.job_id`, row.job_id),
41
+ session_id: hermesRowText(row, 'session_id') || null,
42
+ status: statuses[word], claimed_at: required(file, `${id}.claimed_at`, row.claimed_at),
43
+ started_at: optional(file, `${id}.started_at`, row.started_at),
44
+ finished_at: optional(file, `${id}.finished_at`, row.finished_at),
45
+ error: optional(file, `${id}.error`, row.error),
46
+ obligation_id: hermesRowText(row, 'obligation_id') || null, residue };
47
+ }
48
+
49
+ function instantKey(iso) {
50
+ const digits = iso.split(/[+Z]/u, 1)[0].replace(/[^0-9]/gu, '');
51
+ return digits.length >= 14 ? BigInt(digits.slice(0, 14)) : null;
52
+ }
53
+ function cronInstant(id, job) {
54
+ if (typeof id !== 'string') return null;
55
+ const match = /^cron_(.+)_([0-9]{8})_([0-9]{6})$/su.exec(id);
56
+ return match?.[0].length === id.length && match[1] === job ? BigInt(match[2] + match[3]) : null;
57
+ }
58
+
59
+ /** The native join uses the first claim-window session for an unfinished
60
+ * fire, the last for a finished fire, then follows at most 32 compressions. */
61
+ export function hermesFireSession(sessions, fire) {
62
+ const claimed = instantKey(fire.claimed_at), finished = fire.finished_at == null ? null : instantKey(fire.finished_at);
63
+ if (claimed == null) return null;
64
+ let chosen = null;
65
+ for (const row of sessions) {
66
+ const key = cronInstant(row.id, fire.job_id);
67
+ if (key == null || key < claimed || finished != null && key > finished) continue;
68
+ if (chosen == null || (finished == null ? key < chosen.key : key >= chosen.key)) chosen = { key, id: row.id };
69
+ }
70
+ if (chosen == null) return null;
71
+ let current = chosen.id;
72
+ for (let depth = 0; depth < 32; depth++) {
73
+ if (sessions.find(row => row.id === current)?.end_reason !== 'compression') return current;
74
+ let next = null;
75
+ for (const row of sessions) {
76
+ if (row.parent_session_id !== current) continue;
77
+ const time = typeof row.started_at === 'number' ? row.started_at : 0;
78
+ const id = typeof row.id === 'string' ? row.id : '';
79
+ if (next == null || time > next.time || time === next.time && compare(id, next.id) >= 0) next = { row, time, id };
80
+ }
81
+ if (typeof next?.row.id !== 'string') return current;
82
+ current = next.row.id;
83
+ }
84
+ return current;
85
+ }
86
+
87
+ export function hermesFirePid(fire) {
88
+ let pid = nativeInteger(fire.residue, 'pid');
89
+ if (pid == null && typeof fire.residue.pid === 'string') {
90
+ const text = nativeTrim(fire.residue.pid);
91
+ if (/^\+?[0-9]+$/u.test(text)) pid = BigInt(text);
92
+ }
93
+ return pid != null && pid <= 0xffffffffn ? Number(pid) : null;
94
+ }
95
+
96
+ /** Profiles arrive from the whole validated home. Later fires replace earlier
97
+ * fires for the same session, in profile-key and ledger order. */
98
+ export function hermesActivityFires(profiles) {
99
+ const fires = new Map();
100
+ for (const [, profile] of [...profiles].sort(([left], [right]) => compare(left, right))) {
101
+ for (const fire of profile.fires) if (fire.session_id != null) fires.set(fire.session_id, {
102
+ status: fire.status, pid: hermesFirePid(fire),
103
+ });
104
+ }
105
+ return fires;
106
+ }
@@ -0,0 +1,3 @@
1
+ /** Read the Hermes home dependencies and its derived session/fire links.
2
+ * Failure establishes no activity evidence; no file is written. */
3
+ export function readHermesActivityHome(root: string): Map<string, { status: 'claimed' | 'running' | 'succeeded' | 'failed' | 'unknown'; pid: number | null }>;
@@ -0,0 +1,235 @@
1
+ // The complete Hermes home validity boundary behind an activity reading.
2
+ // The projection keeps fire links, while reading and validating every file
3
+ // dependency of the native home codec. It neither exports nor writes a home.
4
+ import { readFileSync, readdirSync, statSync, lstatSync, realpathSync } from 'node:fs';
5
+ import { toNamespacedPath } from 'node:path';
6
+ import { childPath } from './session-spend-files.mjs';
7
+ import { nativeInteger, parseNativeJson } from './native-json.mjs';
8
+ import { folderTriggerRecords } from './trigger-records.mjs';
9
+ import { folderChannelRecords } from './channel-records.mjs';
10
+ import { nativeTrim } from './text.mjs';
11
+ import { decodeHermesFire, hermesActivityFires } from './hermes-fires.mjs';
12
+ import { decodeHermesObligation, hermesSessionProfile, linkHermesFires } from './hermes-fire-links.mjs';
13
+ import { hermesTableExists, readHermesRows } from './hermes-read-store.mjs';
14
+ import { readHermesYaml } from './hermes-read-yaml.mjs';
15
+ import { validateHermesWorkflow } from './hermes-read-workflow.mjs';
16
+ import { hermesObject, hermesRefuse, hermesJobRecords, validateHermesWorker, validateHermesBudgets, validateHermesRoutes, validateHermesChannels, validateHermesSubscriptions } from './hermes-read-validation.mjs';
17
+ import { validateOrchestratorAgentLayer, orchestratorRoutingSlot } from './orchestrator-read-records.mjs';
18
+
19
+ const utf8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
20
+ const nameUtf8 = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
21
+ const compare = (left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right));
22
+ const selected = (root, path) => childPath(root, path);
23
+ const isFile = path => { try { return statSync(path).isFile(); } catch { return false; } };
24
+ const isDirectory = path => { try { return statSync(path).isDirectory(); } catch { return false; } };
25
+ const display = path => Buffer.isBuffer(path) ? path.toString('utf8') : path;
26
+ function readText(root, name) {
27
+ const path = selected(root, name);
28
+ return isFile(path) ? utf8.decode(readFileSync(path)) : null;
29
+ }
30
+ function dotenv(text) {
31
+ const values = new Map();
32
+ for (const line of (text ?? '').split('\n')) {
33
+ const trimmed = nativeTrim(line); if (!trimmed || trimmed.startsWith('#')) continue;
34
+ const equal = trimmed.indexOf('='); if (equal < 0) continue;
35
+ const key = nativeTrim(trimmed.slice(0, equal)); let value = nativeTrim(trimmed.slice(equal + 1));
36
+ if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) value = value.slice(1, -1);
37
+ values.set(key, value);
38
+ }
39
+ return values;
40
+ }
41
+
42
+ // Access files deliberately ignore unread/malformed entries, as the codec
43
+ // does. Their values remain internal to this read and confer no caller grant.
44
+ function readAccess(root, env, platforms) {
45
+ const allow = new Map(), admins = new Map(), policy = new Map();
46
+ const truthy = value => ['1', 'true', 'yes', 'on'].includes(nativeTrim(value).replace(/[A-Z]/gu, c => c.toLowerCase()));
47
+ const split = value => value.split(',').map(nativeTrim).filter(Boolean);
48
+ const append = (map, key, values) => map.set(key, [...map.get(key) ?? [], ...values]);
49
+ for (const [key, value] of env) {
50
+ if (key.endsWith('_ALLOWED_USERS')) append(allow, key.slice(0, -14).replace(/[A-Z]/gu, c => c.toLowerCase()), split(value));
51
+ else if (key.endsWith('_ALLOW_ALL_USERS') && truthy(value)) policy.set(key.slice(0, -16).replace(/[A-Z]/gu, c => c.toLowerCase()), 'open');
52
+ }
53
+ for (const name of ['platforms/pairing', 'pairing']) {
54
+ let entries; try { entries = readdirSync(selected(root, name), { encoding: 'buffer' }); } catch { continue; }
55
+ for (const entry of entries) {
56
+ const file = entry.toString('utf8'); if (!file.endsWith('-approved.json')) continue;
57
+ try {
58
+ const value = parseNativeJson(utf8.decode(readFileSync(childPath(selected(root, name), entry))));
59
+ if (hermesObject(value)) append(allow, file.slice(0, -14), Object.keys(value));
60
+ } catch { /* unread access metadata establishes no allowlist member */ }
61
+ }
62
+ }
63
+ const global = truthy(env.get('GATEWAY_ALLOW_ALL_USERS') ?? '');
64
+ if (hermesObject(platforms)) for (const [platform, block] of Object.entries(platforms)) {
65
+ if (global) policy.set(platform, 'open');
66
+ for (const key of ['allow_admin_from', 'group_allow_admin_from']) {
67
+ const parent = hermesObject(block.extra) && Object.hasOwn(block.extra, key) ? block.extra : block;
68
+ const value = parent[key];
69
+ const ids = typeof value === 'string' ? split(value) : Array.isArray(value)
70
+ ? value.flatMap((item, index) => typeof item === 'string' ? [item]
71
+ : nativeInteger(value, String(index), { signed: true }) != null ? [String(nativeInteger(value, String(index), { signed: true }))] : []) : [];
72
+ append(admins, platform, ids);
73
+ }
74
+ }
75
+ for (const map of [allow, admins]) for (const [key, values] of map) {
76
+ if (!values.length) map.delete(key); else map.set(key, [...new Set(values)].sort(compare));
77
+ }
78
+ return { allow, admins, policy };
79
+ }
80
+
81
+ const credentialName = (platform, key) => `${platform}_${key}`.replace(/[^A-Za-z0-9]/gu, '_').toUpperCase();
82
+ function channelCredentialNames(platforms, vault) {
83
+ const walk = (platform, block, prefix = '') => {
84
+ for (const [key, value] of Object.entries(block)) {
85
+ if (key === 'enabled' && !prefix) continue;
86
+ if (typeof value === 'string' && /^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/u.test(value)) continue;
87
+ if (key === 'extra' && !prefix && hermesObject(value)) walk(platform, value, 'extra.');
88
+ else if (typeof value === 'string' && /token|secret|key|password|passwd|api_key|app_secret|signing|webhook_url|private/u.test(key.replace(/[A-Z]/gu, char => char.toLowerCase()))) vault.add(credentialName(platform, `${prefix}${key}`));
89
+ }
90
+ };
91
+ if (hermesObject(platforms)) for (const [platform, block] of Object.entries(platforms)) walk(platform, block);
92
+ }
93
+ function profile(name, dir, flavor, vault) {
94
+ const env = dotenv(readText(dir, '.env')), configText = readText(dir, 'config.yaml'), configFile = display(selected(dir, 'config.yaml'));
95
+ const config = configText == null ? Object.create(null) : readHermesYaml(configFile, configText);
96
+ if (!hermesObject(config)) hermesRefuse(configFile, '', 'expected a mapping');
97
+ validateHermesWorker(configFile, config.worker); validateHermesBudgets(configFile, config.budgets);
98
+ const routes = Array.isArray(config.profile_routes) ? config.profile_routes : hermesObject(config.gateway) && Array.isArray(config.gateway.profile_routes) ? config.gateway.profile_routes : [];
99
+ validateHermesRoutes(configFile, routes);
100
+ validateHermesChannels(configFile, config.platforms);
101
+ for (const key of env.keys()) vault.add(key);
102
+ const known = new Set(vault); channelCredentialNames(config.platforms, vault);
103
+ const configPresent = configText != null && !(flavor === 'orchestrator' && [...vault].some(key => !known.has(key)));
104
+ readText(dir, flavor === 'hermes' ? 'SOUL.md' : 'AGENTS.md');
105
+ const jobsText = readText(dir, 'cron/jobs.json'), jobsFile = display(selected(dir, 'cron/jobs.json'));
106
+ const jobs = jobsText == null ? new Map() : hermesJobRecords(jobsFile, parseNativeJson(jobsText));
107
+ const executionFile = selected(dir, 'cron/executions.db');
108
+ const fires = hermesTableExists(executionFile, 'executions')
109
+ ? (readHermesRows(executionFile, 'select * from executions order by claimed_at, id') ?? []).map(row => decodeHermesFire(display(executionFile), row)) : [];
110
+ const subscriptionsText = readText(dir, 'webhook_subscriptions.json');
111
+ const subscriptions = subscriptionsText == null ? Object.create(null) : parseNativeJson(subscriptionsText);
112
+ if (subscriptionsText != null) {
113
+ validateHermesSubscriptions(display(selected(dir, 'webhook_subscriptions.json')), subscriptions);
114
+ for (const [name, subscription] of Object.entries(subscriptions)) if (typeof subscription.secret === 'string' && !/^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/u.test(subscription.secret)) vault.add(credentialName('webhook', `${name}_secret`));
115
+ }
116
+ readAccess(dir, env, config.platforms);
117
+ const store = selected(dir, 'state.db');
118
+ const obligations = hermesTableExists(store, 'delivery_obligations')
119
+ ? (readHermesRows(store, 'select * from delivery_obligations order by created_at, obligation_id') ?? []).map(row => decodeHermesObligation(display(store), row)) : [];
120
+ // The native binding fold never refuses individual rows, but the query's
121
+ // columns/order and read failure remain dependencies of the full load.
122
+ const bindings = new Set();
123
+ if (flavor === 'orchestrator') {
124
+ if (hermesTableExists(store, 'gateway_routing')) {
125
+ const sessions = selected(dir, 'sessions'); let scope;
126
+ const canonical = path => {
127
+ const resolved = realpathSync(path);
128
+ return process.platform === 'win32' ? toNamespacedPath(resolved) : resolved;
129
+ };
130
+ try { scope = canonical(sessions); }
131
+ catch { try { scope = display(selected(canonical(dir), 'sessions')); } catch { scope = display(sessions); } }
132
+ for (const row of readHermesRows(store, 'select session_key, entry_json from gateway_routing where scope = ? order by updated_at', [scope]) ?? []) {
133
+ const slot = orchestratorRoutingSlot(row); if (slot != null) bindings.add(slot);
134
+ }
135
+ }
136
+ } else if (hermesTableExists(store, 'sessions')) readHermesRows(store, 'select * from sessions order by started_at, id');
137
+ return { name, dir, store, fires, jobs, obligations, lenders: [], borrowed: false, config, configPresent, routes, bindings, subscriptions, channelRecords: folderChannelRecords(config, env) };
138
+ }
139
+
140
+ function partitionSharedStore(profiles, root) {
141
+ const store = selected(root, 'state.db'); if (!isFile(store)) return;
142
+ const primary = profiles.get('default');
143
+ for (const name of [...profiles.keys()].filter(name => name !== 'default').sort(compare)) {
144
+ const profile = profiles.get(name); if (isFile(profile.store)) continue;
145
+ profile.borrowed = true; primary.lenders.push(name);
146
+ if (hermesTableExists(store, 'sessions')) readHermesRows(store, 'select * from sessions where profile_name = ? order by started_at, id', [name]);
147
+ profile.obligations = primary.obligations.filter(row => hermesSessionProfile(row.session_key) === name);
148
+ primary.obligations = primary.obligations.filter(row => hermesSessionProfile(row.session_key) !== name);
149
+ }
150
+ }
151
+
152
+ function inventory(root) {
153
+ const owned = new Set(['config.yaml', '.env', 'cron/jobs.json', 'cron/executions.db', 'webhook_subscriptions.json', 'state.db', 'agents.json', 'SOUL.md']);
154
+ const runtime = new Set(['orchestrator.lock', 'orchestrator.sock', 'board.lock', 'board.sock', 'board.epoch', 'service']);
155
+ const walk = (dir, prefix = '') => {
156
+ let entries;
157
+ try { entries = readdirSync(dir, { encoding: 'buffer' }); }
158
+ catch (error) { if (prefix && error.code === 'ENOENT') return; throw error; }
159
+ entries.sort(Buffer.compare);
160
+ for (const entry of entries) {
161
+ const name = entry.toString('utf8'), rel = `${prefix}${name}`, path = childPath(dir, entry);
162
+ if (rel === 'profiles' || ['node_modules', '.git'].includes(name) || rel.startsWith('state.db') || rel.startsWith('cron/executions.db') || runtime.has(rel) || /\.tmp-[0-9]+$/u.test(name)) continue;
163
+ let info; try { info = lstatSync(path); } catch (error) { if (error.code === 'ENOENT') continue; throw error; }
164
+ if (info.isDirectory()) walk(path, `${rel}/`);
165
+ else if (info.isFile() && !owned.has(rel)) { /* the projection carries no file inventory on its wire */ }
166
+ }
167
+ };
168
+ walk(root);
169
+ }
170
+
171
+ function readFolderHome(root, flavor = 'hermes') {
172
+ if (!isDirectory(root)) hermesRefuse(display(root), '', 'not a directory');
173
+ const vault = new Set(), profiles = new Map([['default', profile('default', root, flavor, vault)]]), named = selected(root, 'profiles');
174
+ if (isDirectory(named)) {
175
+ const names = readdirSync(named, { encoding: 'buffer' }).flatMap(entry => {
176
+ let name; try { name = nameUtf8.decode(entry); } catch { return []; }
177
+ return name !== 'node_modules' && !name.startsWith('.') && isDirectory(childPath(named, entry)) ? [name] : [];
178
+ }).sort(compare);
179
+ for (const name of names) {
180
+ if (name === 'default') hermesRefuse(display(selected(named, name)), '', '"default" is the root folder, not a named profile');
181
+ profiles.set(name, profile(name, selected(named, name), flavor, vault));
182
+ }
183
+ }
184
+ const workflow = readText(root, 'workflow.yaml');
185
+ if (workflow != null) validateHermesWorkflow(display(selected(root, 'workflow.yaml')), readHermesYaml(display(selected(root, 'workflow.yaml')), workflow));
186
+ const agents = readText(root, 'agents.json'); // Hermes ignores its contents after the strict UTF-8 file read.
187
+ if (flavor === 'orchestrator' && agents != null) validateOrchestratorAgentLayer(agents);
188
+ if (flavor === 'hermes') partitionSharedStore(profiles, root);
189
+ const rootStore = selected(root, 'state.db');
190
+ linkHermesFires(profiles, profile => {
191
+ const store = isFile(profile.store) ? profile.store : rootStore;
192
+ return hermesTableExists(store, 'sessions') ? readHermesRows(store, 'select id, started_at, end_reason, parent_session_id, session_key from sessions') ?? [] : [];
193
+ });
194
+ if (flavor === 'hermes') for (const profile of profiles.values()) inventory(profile.dir);
195
+ return profiles;
196
+ }
197
+
198
+ export function readHermesActivityHome(root) { return hermesActivityFires(readFolderHome(root)); }
199
+
200
+ export function readChannelFolderHome(root, flavor = 'hermes') {
201
+ return new Map([...readFolderHome(root, flavor)].map(([name, profile]) => [name, profile.channelRecords]));
202
+ }
203
+
204
+ // Only recorded run facts leave this projection; configuration and credentials
205
+ // remain internal to the same complete home validity and linking boundary.
206
+ export function readRunFolderHome(root, flavor = 'hermes') {
207
+ return new Map([...readFolderHome(root, flavor)].map(([name, profile]) => [name, {
208
+ fires: profile.fires, obligations: profile.obligations,
209
+ }]));
210
+ }
211
+
212
+ // Route fields leave only after the complete folder codec has admitted the
213
+ // home. No other configuration or credential value crosses this projection.
214
+ export function readTriggerFolderHome(root, flavor = 'hermes') {
215
+ return new Map([...readFolderHome(root, flavor)].map(([name, profile]) => [name, folderTriggerRecords(profile.subscriptions, profile.config)]));
216
+ }
217
+
218
+ export function readRouteFolderHome(root, flavor = 'hermes') {
219
+ return new Map([...readFolderHome(root, flavor)].map(([name, profile]) => [name, profile.routes]));
220
+ }
221
+
222
+ // Keep credential/configuration values internal to the complete home read.
223
+ // Only the fields of the profile noun cross this projection boundary.
224
+ export function readProfileFolderHome(root, flavor = 'hermes') {
225
+ return new Map([...readFolderHome(root, flavor)].map(([name, profile]) => {
226
+ const rawModel = profile.config.model;
227
+ const text = value => typeof value === 'string' ? value : null;
228
+ const model = flavor === 'hermes' ? text(rawModel) ?? (hermesObject(rawModel) ? text(Object.hasOwn(rawModel, 'default') ? rawModel.default : rawModel.model) : null) : text(profile.config.worker?.model);
229
+ return [name, {
230
+ dir: profile.dir, store: profile.store, configPresent: profile.configPresent,
231
+ routes: profile.routes.map(route => ({ agent: route.agent ?? route.profile })),
232
+ bindings: profile.bindings.size, model, worker: text(profile.config.worker?.harness),
233
+ }];
234
+ }));
235
+ }
@@ -0,0 +1,49 @@
1
+ // The orchestration codec's read-only SQLite row contract. Opening a ledger
2
+ // establishes no owner, and a missing/unopenable table establishes no rows.
3
+ import { DatabaseSync } from 'node:sqlite';
4
+ import { statSync } from 'node:fs';
5
+ import { setNativeInteger, setNativeFloat } from './native-json.mjs';
6
+
7
+ const file = path => { try { return statSync(path).isFile(); } catch { return false; } };
8
+ const text = new TextDecoder('utf-8', { ignoreBOM: true });
9
+ const pathText = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
10
+ function databasePath(path) {
11
+ if (!Buffer.isBuffer(path)) return path;
12
+ // The supported Node 22/23 SQLite constructor takes a string path.
13
+ // Decode only byte-exact UTF-8; newer runtimes keep other byte paths.
14
+ try { return pathText.decode(path); } catch { return path; }
15
+ }
16
+ function open(path) {
17
+ const db = new DatabaseSync(databasePath(path), { readOnly: true, allowExtension: false });
18
+ try { db.exec('PRAGMA busy_timeout=0'); return db; }
19
+ catch (error) { db.close(); throw error; }
20
+ }
21
+
22
+ export function hermesTableExists(path, table) {
23
+ if (!file(path)) return false;
24
+ let db;
25
+ try {
26
+ db = open(path);
27
+ return db.prepare("select name from sqlite_master where type='table' and name=?").get(table) != null;
28
+ } catch { return false; }
29
+ finally { db?.close(); }
30
+ }
31
+
32
+ export function readHermesRows(path, sql, params = []) {
33
+ if (!file(path)) return null;
34
+ let db;
35
+ try {
36
+ db = open(path);
37
+ const statement = db.prepare(sql); statement.setReadBigInts(true);
38
+ return statement.all(...params).map(row => {
39
+ const result = Object.create(null);
40
+ for (const [key, value] of Object.entries(row)) {
41
+ if (typeof value === 'bigint') setNativeInteger(result, key, value);
42
+ else if (typeof value === 'number') setNativeFloat(result, key, value);
43
+ else result[key] = value instanceof Uint8Array ? text.decode(value) : value;
44
+ }
45
+ return result;
46
+ });
47
+ } catch (error) { throw new Error(`${path}: ${error.message}`, { cause: error }); }
48
+ finally { db?.close(); }
49
+ }