@ashlr/hub 2.2.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +59 -2
  3. package/dist/api/plugin.d.ts +1 -0
  4. package/dist/cli/backlog.js +2 -0
  5. package/dist/cli/backlog.js.map +1 -1
  6. package/dist/cli/digest.d.ts +6 -0
  7. package/dist/cli/digest.js +127 -2
  8. package/dist/cli/digest.js.map +1 -1
  9. package/dist/cli/eval-fixtures.d.ts +18 -0
  10. package/dist/cli/eval-fixtures.js +38 -0
  11. package/dist/cli/eval-fixtures.js.map +1 -0
  12. package/dist/cli/eval.d.ts +56 -0
  13. package/dist/cli/eval.js +283 -0
  14. package/dist/cli/eval.js.map +1 -0
  15. package/dist/cli/fleet.d.ts +27 -0
  16. package/dist/cli/fleet.js +222 -0
  17. package/dist/cli/fleet.js.map +1 -0
  18. package/dist/cli/goal.d.ts +15 -0
  19. package/dist/cli/goal.js +91 -0
  20. package/dist/cli/goal.js.map +1 -0
  21. package/dist/cli/help.js +10 -3
  22. package/dist/cli/help.js.map +1 -1
  23. package/dist/cli/inbox.d.ts +6 -0
  24. package/dist/cli/inbox.js +183 -8
  25. package/dist/cli/inbox.js.map +1 -1
  26. package/dist/cli/index.js +42 -0
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/cli/loop.d.ts +16 -0
  29. package/dist/cli/loop.js +66 -0
  30. package/dist/cli/loop.js.map +1 -0
  31. package/dist/cli/mcp.d.ts +12 -0
  32. package/dist/cli/mcp.js +148 -0
  33. package/dist/cli/mcp.js.map +1 -1
  34. package/dist/cli/onboard.d.ts +16 -0
  35. package/dist/cli/onboard.js +71 -0
  36. package/dist/cli/onboard.js.map +1 -1
  37. package/dist/cli/plugins.js +9 -8
  38. package/dist/cli/plugins.js.map +1 -1
  39. package/dist/cli/pulse.d.ts +10 -0
  40. package/dist/cli/pulse.js +261 -0
  41. package/dist/cli/pulse.js.map +1 -1
  42. package/dist/cli/run.js +21 -3
  43. package/dist/cli/run.js.map +1 -1
  44. package/dist/cli/stack.d.ts +34 -0
  45. package/dist/cli/stack.js +221 -0
  46. package/dist/cli/stack.js.map +1 -0
  47. package/dist/cli/update.js +12 -5
  48. package/dist/cli/update.js.map +1 -1
  49. package/dist/core/daemon/loop.d.ts +10 -5
  50. package/dist/core/daemon/loop.js +167 -37
  51. package/dist/core/daemon/loop.js.map +1 -1
  52. package/dist/core/env-bridge.js +3 -2
  53. package/dist/core/env-bridge.js.map +1 -1
  54. package/dist/core/fleet/automerge-pass.d.ts +33 -0
  55. package/dist/core/fleet/automerge-pass.js +61 -0
  56. package/dist/core/fleet/automerge-pass.js.map +1 -0
  57. package/dist/core/fleet/quota.d.ts +70 -0
  58. package/dist/core/fleet/quota.js +198 -0
  59. package/dist/core/fleet/quota.js.map +1 -0
  60. package/dist/core/fleet/router.d.ts +46 -0
  61. package/dist/core/fleet/router.js +121 -0
  62. package/dist/core/fleet/router.js.map +1 -0
  63. package/dist/core/fleet/self.d.ts +58 -0
  64. package/dist/core/fleet/self.js +187 -0
  65. package/dist/core/fleet/self.js.map +1 -0
  66. package/dist/core/fleet/status.d.ts +54 -0
  67. package/dist/core/fleet/status.js +157 -0
  68. package/dist/core/fleet/status.js.map +1 -0
  69. package/dist/core/foundry/provenance.d.ts +78 -0
  70. package/dist/core/foundry/provenance.js +172 -0
  71. package/dist/core/foundry/provenance.js.map +1 -0
  72. package/dist/core/git.d.ts +13 -0
  73. package/dist/core/git.js +26 -0
  74. package/dist/core/git.js.map +1 -1
  75. package/dist/core/inbox/merge.d.ts +151 -0
  76. package/dist/core/inbox/merge.js +858 -0
  77. package/dist/core/inbox/merge.js.map +1 -0
  78. package/dist/core/inbox/store.js +10 -1
  79. package/dist/core/inbox/store.js.map +1 -1
  80. package/dist/core/integrations/markdown.d.ts +66 -0
  81. package/dist/core/integrations/markdown.js +132 -0
  82. package/dist/core/integrations/markdown.js.map +1 -0
  83. package/dist/core/integrations/secrets.d.ts +37 -0
  84. package/dist/core/integrations/secrets.js +67 -0
  85. package/dist/core/integrations/secrets.js.map +1 -0
  86. package/dist/core/integrations/stack.d.ts +47 -0
  87. package/dist/core/integrations/stack.js +107 -0
  88. package/dist/core/integrations/stack.js.map +1 -0
  89. package/dist/core/mcp-native-engineer.d.ts +123 -0
  90. package/dist/core/mcp-native-engineer.js +936 -0
  91. package/dist/core/mcp-native-engineer.js.map +1 -0
  92. package/dist/core/mcp-native.js +20 -42
  93. package/dist/core/mcp-native.js.map +1 -1
  94. package/dist/core/mcp-registry.js +2 -0
  95. package/dist/core/mcp-registry.js.map +1 -1
  96. package/dist/core/observability/codex-source.d.ts +53 -0
  97. package/dist/core/observability/codex-source.js +390 -0
  98. package/dist/core/observability/codex-source.js.map +1 -0
  99. package/dist/core/observability/estimate.d.ts +12 -0
  100. package/dist/core/observability/estimate.js +26 -0
  101. package/dist/core/observability/estimate.js.map +1 -1
  102. package/dist/core/observability/limits.d.ts +52 -0
  103. package/dist/core/observability/limits.js +316 -0
  104. package/dist/core/observability/limits.js.map +1 -0
  105. package/dist/core/observability/usage-source.js +3 -1
  106. package/dist/core/observability/usage-source.js.map +1 -1
  107. package/dist/core/portfolio/scanners.d.ts +1 -0
  108. package/dist/core/portfolio/scanners.js +40 -1
  109. package/dist/core/portfolio/scanners.js.map +1 -1
  110. package/dist/core/providers.js +4 -0
  111. package/dist/core/providers.js.map +1 -1
  112. package/dist/core/quality/health.js +1 -0
  113. package/dist/core/quality/health.js.map +1 -1
  114. package/dist/core/run/agent-loop.d.ts +8 -0
  115. package/dist/core/run/agent-loop.js +24 -14
  116. package/dist/core/run/agent-loop.js.map +1 -1
  117. package/dist/core/run/engine-registry.d.ts +48 -0
  118. package/dist/core/run/engine-registry.js +190 -0
  119. package/dist/core/run/engine-registry.js.map +1 -0
  120. package/dist/core/run/engines.d.ts +17 -3
  121. package/dist/core/run/engines.js +63 -44
  122. package/dist/core/run/engines.js.map +1 -1
  123. package/dist/core/run/learned-router.d.ts +121 -0
  124. package/dist/core/run/learned-router.js +374 -0
  125. package/dist/core/run/learned-router.js.map +1 -0
  126. package/dist/core/run/model-manager.js +3 -3
  127. package/dist/core/run/model-manager.js.map +1 -1
  128. package/dist/core/run/model-profile.d.ts +67 -0
  129. package/dist/core/run/model-profile.js +132 -0
  130. package/dist/core/run/model-profile.js.map +1 -0
  131. package/dist/core/run/orchestrator.d.ts +1 -1
  132. package/dist/core/run/orchestrator.js +642 -510
  133. package/dist/core/run/orchestrator.js.map +1 -1
  134. package/dist/core/run/prompts/budget.d.ts +25 -0
  135. package/dist/core/run/prompts/budget.js +51 -0
  136. package/dist/core/run/prompts/budget.js.map +1 -0
  137. package/dist/core/run/prompts/index.d.ts +14 -0
  138. package/dist/core/run/prompts/index.js +59 -0
  139. package/dist/core/run/prompts/index.js.map +1 -0
  140. package/dist/core/run/prompts/layers.d.ts +26 -0
  141. package/dist/core/run/prompts/layers.js +73 -0
  142. package/dist/core/run/prompts/layers.js.map +1 -0
  143. package/dist/core/run/prompts/roles.d.ts +23 -0
  144. package/dist/core/run/prompts/roles.js +48 -0
  145. package/dist/core/run/prompts/roles.js.map +1 -0
  146. package/dist/core/run/prompts/types.d.ts +38 -0
  147. package/dist/core/run/prompts/types.js +5 -0
  148. package/dist/core/run/prompts/types.js.map +1 -0
  149. package/dist/core/run/provider-client.d.ts +6 -24
  150. package/dist/core/run/provider-client.js +125 -61
  151. package/dist/core/run/provider-client.js.map +1 -1
  152. package/dist/core/run/router.js +8 -0
  153. package/dist/core/run/router.js.map +1 -1
  154. package/dist/core/run/sandboxed-engine.d.ts +66 -0
  155. package/dist/core/run/sandboxed-engine.js +262 -0
  156. package/dist/core/run/sandboxed-engine.js.map +1 -0
  157. package/dist/core/run/verify-commands.d.ts +83 -0
  158. package/dist/core/run/verify-commands.js +234 -0
  159. package/dist/core/run/verify-commands.js.map +1 -0
  160. package/dist/core/run/verify.d.ts +31 -1
  161. package/dist/core/run/verify.js +48 -0
  162. package/dist/core/run/verify.js.map +1 -1
  163. package/dist/core/sandbox/audit.d.ts +27 -0
  164. package/dist/core/sandbox/audit.js +34 -0
  165. package/dist/core/sandbox/audit.js.map +1 -1
  166. package/dist/core/sandbox/confine.d.ts +134 -0
  167. package/dist/core/sandbox/confine.js +370 -0
  168. package/dist/core/sandbox/confine.js.map +1 -0
  169. package/dist/core/types.d.ts +227 -5
  170. package/dist/core/web/api.js +77 -28
  171. package/dist/core/web/api.js.map +1 -1
  172. package/dist/core/web/control.d.ts +116 -0
  173. package/dist/core/web/control.js +307 -0
  174. package/dist/core/web/control.js.map +1 -0
  175. package/dist/core/web/public/app.js +512 -6
  176. package/dist/core/web/public/styles.css +468 -0
  177. package/package.json +1 -1
@@ -0,0 +1,54 @@
1
+ /**
2
+ * M49: fleet control plane + observability — read-only aggregation.
3
+ *
4
+ * `buildFleetStatus(cfg)` is a single READ-ONLY snapshot of the whole fleet:
5
+ * the daemon's liveness + today's spend, per-backend recent dispatch counts and
6
+ * quota status, the backlog queue size, the inbox proposal counts (pending /
7
+ * frontier-pending / applied), recent auto-merges, and whether the global kill
8
+ * switch is engaged.
9
+ *
10
+ * SAFETY: this NEVER mutates anything and NEVER throws. Every underlying source
11
+ * is wrapped in its own try/catch with a sane fallback, so a single broken
12
+ * source (corrupt daemon state, absent backlog, etc.) degrades only its own
13
+ * slice — the rest of the snapshot still resolves. It adds NO capability: it
14
+ * only reads what the daemon/quota/inbox/backlog modules already persist.
15
+ */
16
+ import type { AshlrConfig, EngineId } from '../types.js';
17
+ /** A single backend's recent activity + quota standing. */
18
+ export interface FleetBackendStatus {
19
+ /** The backend id (e.g. 'builtin', 'claude', 'codex'). */
20
+ backend: EngineId;
21
+ /** Dispatches recorded for this backend in the recent window (last 24h). */
22
+ dispatchesRecent: number;
23
+ /** Quota standing: 'unlimited' when no rate limit is configured. */
24
+ quota: 'ok' | 'warn' | 'over' | 'unlimited';
25
+ }
26
+ /** One whole-fleet read-only snapshot. */
27
+ export interface FleetStatus {
28
+ /** ISO timestamp this snapshot was generated. */
29
+ generatedAt: string;
30
+ daemon: {
31
+ running: boolean;
32
+ lastTickAt: string | null;
33
+ todaySpentUsd: number;
34
+ };
35
+ backends: FleetBackendStatus[];
36
+ queue: {
37
+ backlogItems: number;
38
+ };
39
+ proposals: {
40
+ pending: number;
41
+ frontierPending: number;
42
+ applied: number;
43
+ };
44
+ merges: {
45
+ recent: number;
46
+ };
47
+ /** True when the global kill switch is engaged (fleet paused). */
48
+ killed: boolean;
49
+ }
50
+ /**
51
+ * Build a read-only snapshot of the fleet. Async because the backlog scan is
52
+ * async. NEVER throws — each source is independently guarded.
53
+ */
54
+ export declare function buildFleetStatus(cfg: AshlrConfig): Promise<FleetStatus>;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * M49: fleet control plane + observability — read-only aggregation.
3
+ *
4
+ * `buildFleetStatus(cfg)` is a single READ-ONLY snapshot of the whole fleet:
5
+ * the daemon's liveness + today's spend, per-backend recent dispatch counts and
6
+ * quota status, the backlog queue size, the inbox proposal counts (pending /
7
+ * frontier-pending / applied), recent auto-merges, and whether the global kill
8
+ * switch is engaged.
9
+ *
10
+ * SAFETY: this NEVER mutates anything and NEVER throws. Every underlying source
11
+ * is wrapped in its own try/catch with a sane fallback, so a single broken
12
+ * source (corrupt daemon state, absent backlog, etc.) degrades only its own
13
+ * slice — the rest of the snapshot still resolves. It adds NO capability: it
14
+ * only reads what the daemon/quota/inbox/backlog modules already persist.
15
+ */
16
+ /** Recent window for dispatch + merge counting: the last 24 hours. */
17
+ const RECENT_WINDOW_MS = 24 * 60 * 60 * 1000;
18
+ /**
19
+ * Build a read-only snapshot of the fleet. Async because the backlog scan is
20
+ * async. NEVER throws — each source is independently guarded.
21
+ */
22
+ export async function buildFleetStatus(cfg) {
23
+ const generatedAt = new Date().toISOString();
24
+ // ── daemon ────────────────────────────────────────────────────────────────
25
+ let daemon = {
26
+ running: false,
27
+ lastTickAt: null,
28
+ todaySpentUsd: 0,
29
+ };
30
+ // Recent ticks are reused for merge counting below.
31
+ let recentTicks = [];
32
+ try {
33
+ const { loadDaemonState } = await import('../daemon/state.js');
34
+ const ds = loadDaemonState();
35
+ daemon = {
36
+ running: ds.running === true,
37
+ lastTickAt: ds.lastTickAt ?? null,
38
+ todaySpentUsd: typeof ds.todaySpentUsd === 'number' ? ds.todaySpentUsd : 0,
39
+ };
40
+ recentTicks = Array.isArray(ds.ticks) ? ds.ticks : [];
41
+ }
42
+ catch {
43
+ // leave fallback
44
+ }
45
+ // ── backends ────────────────────────────────────────────────────────────────
46
+ const allowed = cfg.foundry?.allowedBackends ?? ['builtin'];
47
+ const backends = [];
48
+ for (const backend of allowed) {
49
+ let dispatchesRecent = 0;
50
+ let quota = 'unlimited';
51
+ try {
52
+ const { usesInWindow, evalQuota } = await import('./quota.js');
53
+ // Prefer the quota ledger (authoritative for rate-limit accounting).
54
+ dispatchesRecent = usesInWindow(backend, RECENT_WINDOW_MS);
55
+ // Quota standing: 'unlimited' when no limit is configured for this backend.
56
+ const limit = cfg.foundry?.limits?.[backend];
57
+ quota = limit ? evalQuota(backend, cfg) : 'unlimited';
58
+ }
59
+ catch {
60
+ // Ledger unavailable — fall back to summing recent tick.backends counts.
61
+ try {
62
+ dispatchesRecent = sumRecentBackend(recentTicks, backend);
63
+ }
64
+ catch {
65
+ dispatchesRecent = 0;
66
+ }
67
+ quota = 'unlimited';
68
+ }
69
+ backends.push({ backend, dispatchesRecent, quota });
70
+ }
71
+ // ── queue (backlog size) ──────────────────────────────────────────────────
72
+ let backlogItems = 0;
73
+ try {
74
+ const { buildBacklog } = await import('../portfolio/backlog.js');
75
+ const backlog = await buildBacklog();
76
+ backlogItems = Array.isArray(backlog.items) ? backlog.items.length : 0;
77
+ }
78
+ catch {
79
+ backlogItems = 0;
80
+ }
81
+ // ── proposals (pending / frontier-pending / applied) ──────────────────────
82
+ let pending = 0;
83
+ let frontierPending = 0;
84
+ let applied = 0;
85
+ try {
86
+ const { listProposals } = await import('../inbox/store.js');
87
+ const all = listProposals();
88
+ for (const p of all) {
89
+ if (p.status === 'pending') {
90
+ pending++;
91
+ if (p.engineTier === 'frontier')
92
+ frontierPending++;
93
+ }
94
+ else if (p.status === 'applied') {
95
+ applied++;
96
+ }
97
+ }
98
+ }
99
+ catch {
100
+ pending = 0;
101
+ frontierPending = 0;
102
+ applied = 0;
103
+ }
104
+ // ── merges (recent auto-merges across recent ticks) ───────────────────────
105
+ let mergesRecent = 0;
106
+ try {
107
+ const cutoff = Date.now() - RECENT_WINDOW_MS;
108
+ for (const t of recentTicks) {
109
+ if (typeof t.merged !== 'number' || t.merged <= 0)
110
+ continue;
111
+ const ts = t.ts ? Date.parse(t.ts) : NaN;
112
+ // Count when within the recent window, or when the tick has no parseable
113
+ // timestamp (be inclusive rather than silently drop a real merge).
114
+ if (Number.isNaN(ts) || ts >= cutoff)
115
+ mergesRecent += t.merged;
116
+ }
117
+ }
118
+ catch {
119
+ mergesRecent = 0;
120
+ }
121
+ // ── kill switch ───────────────────────────────────────────────────────────
122
+ let killed = false;
123
+ try {
124
+ const { killSwitchOn } = await import('../sandbox/policy.js');
125
+ killed = killSwitchOn() === true;
126
+ }
127
+ catch {
128
+ killed = false;
129
+ }
130
+ return {
131
+ generatedAt,
132
+ daemon,
133
+ backends,
134
+ queue: { backlogItems },
135
+ proposals: { pending, frontierPending, applied },
136
+ merges: { recent: mergesRecent },
137
+ killed,
138
+ };
139
+ }
140
+ /**
141
+ * Sum `tick.backends[backend]` across recent ticks (within RECENT_WINDOW_MS).
142
+ * Pure fallback for when the quota ledger is unavailable.
143
+ */
144
+ function sumRecentBackend(ticks, backend) {
145
+ const cutoff = Date.now() - RECENT_WINDOW_MS;
146
+ let sum = 0;
147
+ for (const t of ticks) {
148
+ const n = t.backends?.[backend];
149
+ if (typeof n !== 'number' || n <= 0)
150
+ continue;
151
+ const ts = t.ts ? Date.parse(t.ts) : NaN;
152
+ if (Number.isNaN(ts) || ts >= cutoff)
153
+ sum += n;
154
+ }
155
+ return sum;
156
+ }
157
+ //# sourceMappingURL=status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.js","sourceRoot":"","sources":["../../../src/core/fleet/status.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * provenance.ts — M47.1: HMAC-signed provenance for inbox proposals (H3).
3
+ *
4
+ * ╔══════════════════════════════════════════════════════════════════════════╗
5
+ * ║ THREAT MODEL — forged frontier merge-authority on a disk record. ║
6
+ * ║ ║
7
+ * ║ merge.ts Gate 4 (evaluateMergeAuthority) trusts proposal.engineTier / ║
8
+ * ║ proposal.engineModel as read from ~/.ashlr/inbox/<id>.json. The agent MCP ║
9
+ * ║ surface cannot set those fields (they are stamped server-side), but a ║
10
+ * ║ LOCAL / in-process writer with filesystem access to the inbox COULD forge ║
11
+ * ║ a record claiming `{engineTier:'frontier', engineModel:'codex:gpt-5.5'}` ║
12
+ * ║ paired with an arbitrary diff, and slip it past the authority gate. ║
13
+ * ║ ║
14
+ * ║ MITIGATION: the sandboxed producer (the ONLY legitimate origin of a ║
15
+ * ║ frontier proposal) HMAC-signs `${engineModel}|${engineTier}|${diffHash}` ║
16
+ * ║ with a host-local secret key (~/.ashlr/foundry/provenance.key, mode 0600) ║
17
+ * ║ and stores the signature + diffHash on the record. The merge gate ║
18
+ * ║ re-derives the HMAC and FAILS CLOSED on any mismatch. A forger who cannot ║
19
+ * ║ read the key cannot mint a valid signature; binding the diff hash into the ║
20
+ * ║ MAC also prevents pairing a stolen signature with a swapped diff. ║
21
+ * ║ ║
22
+ * ║ RESIDUAL: an attacker who can READ ~/.ashlr/foundry/provenance.key can ║
23
+ * ║ sign arbitrary records — the key is only as strong as the filesystem ║
24
+ * ║ permissions protecting it (0600). This raises the bar from "any local ║
25
+ * ║ writer" to "an attacker who already owns the user's home dir secrets". ║
26
+ * ║ ║
27
+ * ║ INVARIANTS: every exported fn NEVER throws (verify catches → fail-closed); ║
28
+ * ║ comparisons are constant-time; node:crypto only. ║
29
+ * ╚══════════════════════════════════════════════════════════════════════════╝
30
+ */
31
+ /**
32
+ * Absolute path to the provenance HMAC key: ~/.ashlr/foundry/provenance.key.
33
+ * Re-resolved at call time so tests can relocate HOME between invocations.
34
+ */
35
+ export declare function provenanceKeyPath(): string;
36
+ /**
37
+ * Load the host-local provenance key, creating it on first use.
38
+ *
39
+ * - If the key file exists, read and return its bytes.
40
+ * - Else generate 32 random bytes, `mkdir -p ~/.ashlr/foundry`, and write the
41
+ * key with mode 0600 via an atomic tmp-write + rename, then return it.
42
+ *
43
+ * NEVER throws. On any I/O failure it falls back to a process-lifetime
44
+ * ephemeral key. That fallback is degenerate by design: signatures made with it
45
+ * will not match a persistent key at verify time, so the merge gate fails
46
+ * closed rather than trusting an unverifiable provenance claim.
47
+ */
48
+ export declare function loadOrCreateKey(): Buffer;
49
+ /** SHA-256 hex digest of the diff string. Pure; never throws. */
50
+ export declare function hashDiff(diff: string): string;
51
+ /**
52
+ * HMAC-SHA256(key, `${engineModel}|${engineTier}|${diffHash}`) as hex.
53
+ * Binds the trust tuple to a concrete diff so a stolen signature cannot be
54
+ * re-paired with a different diff. Never throws.
55
+ */
56
+ export declare function signProvenance(engineModel: string, engineTier: string, diffHash: string): string;
57
+ export interface ProvenanceVerdict {
58
+ ok: boolean;
59
+ reason: string;
60
+ }
61
+ /**
62
+ * Verify a proposal's signed provenance. FAIL-CLOSED in every doubtful case.
63
+ *
64
+ * Rejects when:
65
+ * - any of engineModel / engineTier / diff / diffHash / provenanceSig is missing;
66
+ * - hashDiff(diff) !== diffHash (the stored diff was tampered after signing);
67
+ * - the recomputed HMAC does not equal provenanceSig (constant-time compare;
68
+ * a length mismatch counts as a non-match).
69
+ *
70
+ * ok:true ONLY when every check passes. NEVER throws (any error → fail-closed).
71
+ */
72
+ export declare function verifyProvenance(p: {
73
+ engineModel?: string;
74
+ engineTier?: string;
75
+ diff?: string;
76
+ diffHash?: string;
77
+ provenanceSig?: string;
78
+ }): ProvenanceVerdict;
@@ -0,0 +1,172 @@
1
+ /**
2
+ * provenance.ts — M47.1: HMAC-signed provenance for inbox proposals (H3).
3
+ *
4
+ * ╔══════════════════════════════════════════════════════════════════════════╗
5
+ * ║ THREAT MODEL — forged frontier merge-authority on a disk record. ║
6
+ * ║ ║
7
+ * ║ merge.ts Gate 4 (evaluateMergeAuthority) trusts proposal.engineTier / ║
8
+ * ║ proposal.engineModel as read from ~/.ashlr/inbox/<id>.json. The agent MCP ║
9
+ * ║ surface cannot set those fields (they are stamped server-side), but a ║
10
+ * ║ LOCAL / in-process writer with filesystem access to the inbox COULD forge ║
11
+ * ║ a record claiming `{engineTier:'frontier', engineModel:'codex:gpt-5.5'}` ║
12
+ * ║ paired with an arbitrary diff, and slip it past the authority gate. ║
13
+ * ║ ║
14
+ * ║ MITIGATION: the sandboxed producer (the ONLY legitimate origin of a ║
15
+ * ║ frontier proposal) HMAC-signs `${engineModel}|${engineTier}|${diffHash}` ║
16
+ * ║ with a host-local secret key (~/.ashlr/foundry/provenance.key, mode 0600) ║
17
+ * ║ and stores the signature + diffHash on the record. The merge gate ║
18
+ * ║ re-derives the HMAC and FAILS CLOSED on any mismatch. A forger who cannot ║
19
+ * ║ read the key cannot mint a valid signature; binding the diff hash into the ║
20
+ * ║ MAC also prevents pairing a stolen signature with a swapped diff. ║
21
+ * ║ ║
22
+ * ║ RESIDUAL: an attacker who can READ ~/.ashlr/foundry/provenance.key can ║
23
+ * ║ sign arbitrary records — the key is only as strong as the filesystem ║
24
+ * ║ permissions protecting it (0600). This raises the bar from "any local ║
25
+ * ║ writer" to "an attacker who already owns the user's home dir secrets". ║
26
+ * ║ ║
27
+ * ║ INVARIANTS: every exported fn NEVER throws (verify catches → fail-closed); ║
28
+ * ║ comparisons are constant-time; node:crypto only. ║
29
+ * ╚══════════════════════════════════════════════════════════════════════════╝
30
+ */
31
+ import { createHmac, createHash, randomBytes, timingSafeEqual, } from 'node:crypto';
32
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
33
+ import { homedir } from 'node:os';
34
+ import { join } from 'node:path';
35
+ // ---------------------------------------------------------------------------
36
+ // Key management
37
+ // ---------------------------------------------------------------------------
38
+ /**
39
+ * In-memory ephemeral key used ONLY as a last-resort fallback when the key file
40
+ * can neither be read nor created (e.g. a read-only home). A signature made with
41
+ * this key will not verify against a freshly-loaded persistent key, so the
42
+ * effect is fail-closed at the merge gate — exactly the safe outcome.
43
+ */
44
+ let _ephemeralKey = null;
45
+ /**
46
+ * Absolute path to the provenance HMAC key: ~/.ashlr/foundry/provenance.key.
47
+ * Re-resolved at call time so tests can relocate HOME between invocations.
48
+ */
49
+ export function provenanceKeyPath() {
50
+ return join(homedir(), '.ashlr', 'foundry', 'provenance.key');
51
+ }
52
+ /**
53
+ * Load the host-local provenance key, creating it on first use.
54
+ *
55
+ * - If the key file exists, read and return its bytes.
56
+ * - Else generate 32 random bytes, `mkdir -p ~/.ashlr/foundry`, and write the
57
+ * key with mode 0600 via an atomic tmp-write + rename, then return it.
58
+ *
59
+ * NEVER throws. On any I/O failure it falls back to a process-lifetime
60
+ * ephemeral key. That fallback is degenerate by design: signatures made with it
61
+ * will not match a persistent key at verify time, so the merge gate fails
62
+ * closed rather than trusting an unverifiable provenance claim.
63
+ */
64
+ export function loadOrCreateKey() {
65
+ const keyPath = provenanceKeyPath();
66
+ try {
67
+ if (existsSync(keyPath)) {
68
+ const buf = readFileSync(keyPath);
69
+ if (buf.length > 0)
70
+ return buf;
71
+ // A truncated/empty key file is unusable — regenerate below.
72
+ }
73
+ }
74
+ catch {
75
+ // fall through to create / ephemeral
76
+ }
77
+ try {
78
+ const dir = join(homedir(), '.ashlr', 'foundry');
79
+ if (!existsSync(dir)) {
80
+ mkdirSync(dir, { recursive: true });
81
+ }
82
+ const key = randomBytes(32);
83
+ const tmp = keyPath + '.tmp';
84
+ // mode 0600: owner read/write only — the file holds the signing secret.
85
+ writeFileSync(tmp, key, { mode: 0o600 });
86
+ renameSync(tmp, keyPath);
87
+ return key;
88
+ }
89
+ catch {
90
+ // Last resort: an ephemeral, process-local key. Persisted records signed
91
+ // with this will NOT verify after a fresh load → fail-closed at merge.
92
+ if (!_ephemeralKey)
93
+ _ephemeralKey = randomBytes(32);
94
+ return _ephemeralKey;
95
+ }
96
+ }
97
+ // ---------------------------------------------------------------------------
98
+ // Hashing + signing
99
+ // ---------------------------------------------------------------------------
100
+ /** SHA-256 hex digest of the diff string. Pure; never throws. */
101
+ export function hashDiff(diff) {
102
+ return createHash('sha256').update(diff, 'utf8').digest('hex');
103
+ }
104
+ /**
105
+ * HMAC-SHA256(key, `${engineModel}|${engineTier}|${diffHash}`) as hex.
106
+ * Binds the trust tuple to a concrete diff so a stolen signature cannot be
107
+ * re-paired with a different diff. Never throws.
108
+ */
109
+ export function signProvenance(engineModel, engineTier, diffHash) {
110
+ const key = loadOrCreateKey();
111
+ const payload = `${engineModel}|${engineTier}|${diffHash}`;
112
+ return createHmac('sha256', key).update(payload, 'utf8').digest('hex');
113
+ }
114
+ /** Constant-time string compare over equal-length hex; false if lengths differ. */
115
+ function constantTimeEqual(a, b) {
116
+ const ab = Buffer.from(a, 'utf8');
117
+ const bb = Buffer.from(b, 'utf8');
118
+ // timingSafeEqual requires equal-length buffers — a length mismatch is itself
119
+ // a non-match, so short-circuit (without leaking timing on the bytes).
120
+ if (ab.length !== bb.length)
121
+ return false;
122
+ return timingSafeEqual(ab, bb);
123
+ }
124
+ /**
125
+ * Verify a proposal's signed provenance. FAIL-CLOSED in every doubtful case.
126
+ *
127
+ * Rejects when:
128
+ * - any of engineModel / engineTier / diff / diffHash / provenanceSig is missing;
129
+ * - hashDiff(diff) !== diffHash (the stored diff was tampered after signing);
130
+ * - the recomputed HMAC does not equal provenanceSig (constant-time compare;
131
+ * a length mismatch counts as a non-match).
132
+ *
133
+ * ok:true ONLY when every check passes. NEVER throws (any error → fail-closed).
134
+ */
135
+ export function verifyProvenance(p) {
136
+ try {
137
+ if (!p.engineModel) {
138
+ return { ok: false, reason: 'missing engineModel' };
139
+ }
140
+ if (!p.engineTier) {
141
+ return { ok: false, reason: 'missing engineTier' };
142
+ }
143
+ if (p.diff === undefined || p.diff === null) {
144
+ return { ok: false, reason: 'missing diff' };
145
+ }
146
+ if (!p.diffHash) {
147
+ return { ok: false, reason: 'missing diffHash' };
148
+ }
149
+ if (!p.provenanceSig) {
150
+ return { ok: false, reason: 'missing provenanceSig' };
151
+ }
152
+ // The stored diff must hash to the recorded diffHash — otherwise the diff
153
+ // was swapped after signing (the signature binds the tuple to diffHash, not
154
+ // to the diff bytes directly).
155
+ const recomputedHash = hashDiff(p.diff);
156
+ if (!constantTimeEqual(recomputedHash, p.diffHash)) {
157
+ return { ok: false, reason: 'diff hash mismatch (diff tampered after signing)' };
158
+ }
159
+ const expectedSig = signProvenance(p.engineModel, p.engineTier, p.diffHash);
160
+ if (!constantTimeEqual(expectedSig, p.provenanceSig)) {
161
+ return { ok: false, reason: 'provenance signature mismatch' };
162
+ }
163
+ return { ok: true, reason: 'provenance signature valid' };
164
+ }
165
+ catch (err) {
166
+ return {
167
+ ok: false,
168
+ reason: `provenance verify error: ${err instanceof Error ? err.message : String(err)}`,
169
+ };
170
+ }
171
+ }
172
+ //# sourceMappingURL=provenance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provenance.js","sourceRoot":"","sources":["../../../src/core/foundry/provenance.ts"],"names":[],"mappings":"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"}
@@ -19,6 +19,19 @@ export declare function isRepo(dir: string): boolean;
19
19
  * path is not a repo or git is unavailable.
20
20
  */
21
21
  export declare function getGitStatus(repoPath: string): GitStatus | null;
22
+ /**
23
+ * Resolve the repository's default branch name (the integration target for an
24
+ * auto-merge). Resolution order:
25
+ * 1. `git symbolic-ref --short refs/remotes/origin/HEAD` → strip `origin/`.
26
+ * This is the remote's authoritative default branch when a remote exists.
27
+ * 2. `git rev-parse --abbrev-ref HEAD` → the currently checked-out branch.
28
+ * 3. Final fallback: 'main'.
29
+ *
30
+ * Never throws (each git() call already swallows errors with a 5s timeout). A
31
+ * detached HEAD yields 'HEAD' from step 2, which we treat as unresolved and fall
32
+ * through to 'main'.
33
+ */
34
+ export declare function defaultBranch(repoPath: string): string;
22
35
  /**
23
36
  * Parses the `origin` remote URL of the repo at `repoPath` and extracts the
24
37
  * GitHub org (or user) name. Returns `{ remote, org }` where either may be
package/dist/core/git.js CHANGED
@@ -86,6 +86,32 @@ export function getGitStatus(repoPath) {
86
86
  const lastCommit = git(repoPath, ['log', '-1', '--format=%cI']) || null;
87
87
  return { branch, dirty, ahead, behind, lastCommit };
88
88
  }
89
+ /**
90
+ * Resolve the repository's default branch name (the integration target for an
91
+ * auto-merge). Resolution order:
92
+ * 1. `git symbolic-ref --short refs/remotes/origin/HEAD` → strip `origin/`.
93
+ * This is the remote's authoritative default branch when a remote exists.
94
+ * 2. `git rev-parse --abbrev-ref HEAD` → the currently checked-out branch.
95
+ * 3. Final fallback: 'main'.
96
+ *
97
+ * Never throws (each git() call already swallows errors with a 5s timeout). A
98
+ * detached HEAD yields 'HEAD' from step 2, which we treat as unresolved and fall
99
+ * through to 'main'.
100
+ */
101
+ export function defaultBranch(repoPath) {
102
+ const sym = git(repoPath, ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']);
103
+ if (sym) {
104
+ // e.g. "origin/main" → "main"
105
+ const slash = sym.indexOf('/');
106
+ const name = slash >= 0 ? sym.slice(slash + 1) : sym;
107
+ if (name)
108
+ return name;
109
+ }
110
+ const cur = git(repoPath, ['rev-parse', '--abbrev-ref', 'HEAD']);
111
+ if (cur && cur !== 'HEAD')
112
+ return cur;
113
+ return 'main';
114
+ }
89
115
  /**
90
116
  * Parses the `origin` remote URL of the repo at `repoPath` and extracts the
91
117
  * GitHub org (or user) name. Returns `{ remote, org }` where either may be
@@ -1 +1 @@
1
- {"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/core/git.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/core/git.ts"],"names":[],"mappings":"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"}