thumbgate 1.35.0 → 1.37.1
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/.agents/skills/cyberstrike-compare-not-clone/SKILL.md +36 -0
- package/.agents/skills/gitlab-sandbox-allowlist-not-trust/SKILL.md +77 -0
- package/.agents/skills/jit-harness-compare-not-clone/SKILL.md +34 -0
- package/.agents/skills/openui-catalog-compose-honesty/SKILL.md +64 -0
- package/.agents/skills/zvec-grep-compare-not-clone/SKILL.md +34 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/llms.txt +1 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/CONTRIBUTING.md +95 -0
- package/README.md +195 -632
- package/THIRD_PARTY_NOTICES.md +89 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/future-agi/.mcp.json +8 -0
- package/adapters/future-agi/FUTURE_AGI.md +23 -0
- package/adapters/future-agi/config.toml +3 -0
- package/adapters/future-agi/future-agi-bridge.js +9 -0
- package/adapters/future-agi/opencode.json +8 -0
- package/adapters/herdr/herdr-plugin.toml +18 -0
- package/adapters/mcp/server-stdio.js +238 -25
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/workos/WORKOS.md +52 -0
- package/bin/cli.js +336 -3
- package/bin/futureagi-bridge +9 -0
- package/config/gate-templates.json +581 -4
- package/config/gates/actor-critic-audit.json +34 -0
- package/config/gates/default.json +9 -3
- package/config/gates/five-walls-governance.json +34 -0
- package/config/gates/future-agi-guardrails.json +34 -0
- package/config/gates/radware-threat-defense-2026.json +61 -0
- package/config/gates/simatree-data-governance.json +33 -0
- package/config/mcp-allowlists.json +4 -0
- package/config/merge-quality-checks.json +6 -0
- package/config/model-candidates.json +312 -29
- package/config/model-tiers.json +18 -0
- package/config/post-deploy-marketing-pages.json +10 -0
- package/config/progressive/01-wire-only.json +11 -0
- package/config/progressive/02-dashboard-empty-ok.json +10 -0
- package/config/progressive/03-one-lesson.json +10 -0
- package/config/progressive/04-warn-fires.json +11 -0
- package/config/progressive/05-strict-optional.json +11 -0
- package/config/progressive/README.md +15 -0
- package/config/schemas/broker-execution-receipt.schema.json +139 -0
- package/config/schemas/provider-execution-attestation-v1.schema.json +58 -0
- package/conformance/provider-attestation/vectors.json +320 -0
- package/docs/specs/provider-execution-attestation-v1.md +69 -0
- package/openapi/openapi.yaml +15 -0
- package/package.json +390 -148
- package/public/about.html +2 -2
- package/public/ai-malpractice-prevention.html +7 -7
- package/public/blog/a-10-dollar-vps-is-not-a-computer.html +143 -0
- package/public/blog/a-receipt-is-not-world-state.html +388 -0
- package/public/blog/git-at-agent-scale.html +374 -0
- package/public/blog/no-llm-in-the-gate.html +133 -0
- package/public/blog.html +80 -0
- package/public/case-studies.html +16 -1
- package/public/compare.html +28 -0
- package/public/diagnostic.html +216 -7
- package/public/docs/connectors.html +39 -0
- package/public/federal.html +2 -2
- package/public/founders.html +639 -0
- package/public/index.html +87 -9
- package/public/install.html +8 -8
- package/public/learn.html +39 -0
- package/public/numbers.html +2 -2
- package/public/peter.html +310 -0
- package/public/platform-partners.html +119 -0
- package/public/pricing.html +24 -3
- package/public/privacy.html +117 -0
- package/public/pro.html +17 -0
- package/public/support.html +62 -0
- package/public/terms.html +130 -0
- package/public/third-party-notices.html +95 -0
- package/public/yt.html +351 -0
- package/scripts/action-receipts.js +133 -3
- package/scripts/adaptive-governance-arena.js +349 -0
- package/scripts/admin-override.js +205 -0
- package/scripts/agent-action-inventory.js +869 -0
- package/scripts/agent-audit-trace.js +42 -2
- package/scripts/agent-egress-policy.js +1117 -0
- package/scripts/agent-memory-lifecycle.js +141 -2
- package/scripts/agent-operations-planner.js +441 -1
- package/scripts/agent-readiness.js +68 -0
- package/scripts/agent-security-central.js +647 -0
- package/scripts/allowlist-bridge-honesty.js +417 -0
- package/scripts/async-job-runner.js +102 -11
- package/scripts/audit-trail.js +212 -0
- package/scripts/billing.js +1 -1
- package/scripts/broker-execution-receipts.js +719 -0
- package/scripts/budget-aware-gates-proof.js +423 -0
- package/scripts/claude-feedback-sync.js +29 -3
- package/scripts/claw-harness-production.js +237 -0
- package/scripts/cli-schema.js +163 -1
- package/scripts/codex-runbook-flywheel.js +318 -0
- package/scripts/context-footprint.js +186 -0
- package/scripts/contextfs.js +143 -61
- package/scripts/dashboard.js +251 -32
- package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
- package/scripts/docker-sandbox-planner.js +18 -0
- package/scripts/double-blind-eval-protocol.js +252 -0
- package/scripts/edotenv-rl-gateway.js +259 -0
- package/scripts/ensure-production-search-corpus.js +162 -0
- package/scripts/eval-holdout.js +311 -0
- package/scripts/feedback-aggregate.js +21 -2
- package/scripts/feedback-loop.js +87 -5
- package/scripts/feedback-quality.js +9 -0
- package/scripts/file-ledger-lock.js +4 -1
- package/scripts/financial-control-plane.js +41 -1
- package/scripts/find-dormant-requires.js +118 -0
- package/scripts/fs-utils.js +84 -8
- package/scripts/gates-engine.js +810 -63
- package/scripts/generate-case-study-outreach.js +24 -15
- package/scripts/git-at-scale.js +628 -0
- package/scripts/governance-conflict-audit.js +1650 -0
- package/scripts/governance-difficulty-curriculum.js +328 -0
- package/scripts/graphrag-retrieval.js +275 -0
- package/scripts/gurobi-optimizer.js +324 -0
- package/scripts/gurobi_optimizer.py +485 -0
- package/scripts/harness-selector.js +82 -1
- package/scripts/hidden-entry-points.js +284 -0
- package/scripts/human-escalation.js +199 -1
- package/scripts/hybrid-feedback-context.js +152 -19
- package/scripts/intent-governed-execution.js +602 -0
- package/scripts/intervention-policy.js +123 -20
- package/scripts/jit-harness-compose.js +628 -0
- package/scripts/jsonl-watcher.js +10 -0
- package/scripts/lesson-embedding-index.js +95 -12
- package/scripts/lesson-retrieval.js +105 -19
- package/scripts/local-model-profile.js +19 -2
- package/scripts/mailer/resend-mailer.js +1 -1
- package/scripts/matryoshka-embedding.js +235 -0
- package/scripts/mcp-oauth.js +42 -4
- package/scripts/mcp-session-handles.js +1016 -0
- package/scripts/mcp-wiring-doctor.js +314 -0
- package/scripts/memory-firewall.js +115 -2
- package/scripts/memory-scope-readiness.js +299 -0
- package/scripts/memory-vs-rag-route.js +161 -0
- package/scripts/model-tier-router.js +148 -21
- package/scripts/nvidia-specdecode-al-doctor.js +536 -0
- package/scripts/openui-catalog-compose-honesty.js +593 -0
- package/scripts/operational-integrity.js +19 -1
- package/scripts/override-audit.js +213 -0
- package/scripts/package-manager-honesty-doctor.js +458 -0
- package/scripts/pr-manager.js +63 -1
- package/scripts/prove-herdr-adapter.js +52 -0
- package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
- package/scripts/prove-workos.js +73 -0
- package/scripts/provider-attestation-conformance.js +192 -0
- package/scripts/provider-receipt-contract.js +136 -0
- package/scripts/qwen38-max-cost-optimizer.js +401 -0
- package/scripts/radware-threat-defense.js +280 -0
- package/scripts/rag-embedding-identity.js +221 -0
- package/scripts/rag-precision-guardrails.js +112 -2
- package/scripts/remote-feedback-capture.js +159 -0
- package/scripts/research-agent-harness.js +256 -0
- package/scripts/rsi-safety-hillclimb.js +200 -0
- package/scripts/rule-sprawl.js +188 -0
- package/scripts/schedule-manager.js +147 -0
- package/scripts/self-heal.js +8 -0
- package/scripts/session-lease.js +415 -0
- package/scripts/simatree-data-governance.js +347 -0
- package/scripts/slo-alert-engine.js +172 -7
- package/scripts/solver-parity.js +539 -0
- package/scripts/stealth-memory-injection-gate.js +333 -0
- package/scripts/switchyard-router.js +366 -0
- package/scripts/telemetry-analytics.js +84 -27
- package/scripts/temporal-decay-weighting.js +138 -0
- package/scripts/test-all.js +165 -0
- package/scripts/token-savings.js +42 -0
- package/scripts/tool-kpi-tracker.js +108 -5
- package/scripts/tool-registry.js +193 -5
- package/scripts/universal-claim-evaluator.js +14 -2
- package/scripts/vector-store.js +279 -9
- package/scripts/workflow-notebook.js +391 -0
- package/scripts/workflow-sentinel.js +111 -12
- package/scripts/workos-production-guard.js +260 -0
- package/scripts/workspace-search-route.js +515 -0
- package/server.json +2 -2
- package/src/agent-identity-boundary.js +76 -0
- package/src/agent-retrieval-cache.js +155 -0
- package/src/alert-noise-ledger.js +502 -0
- package/src/api/server.js +724 -153
- package/src/git-fast-cache.js +220 -0
- package/src/git-wal-sync.js +156 -0
- package/src/hash-anchored-edit.js +82 -0
- package/src/hermes-platform-protocol.js +475 -0
- package/src/hermes-sync-plane.js +241 -0
- package/src/index.js +30 -1
- package/src/iso42001-compliance-guard.js +97 -0
- package/src/latency-budget.js +244 -0
- package/src/mcp-writeguard.js +316 -0
- package/src/miminions-adapter.js +106 -0
- package/src/pipeline-compass.js +104 -0
- package/src/ppl-alert-pipeline.js +284 -0
- package/src/rendezvous-router.js +90 -0
- package/src/security-questionnaire.js +195 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { execFileSync } = require('node:child_process');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GitFastCache — High-performance shadow git metadata cache.
|
|
9
|
+
* Inspired by Cursor's Continuity/Spokes architecture (Aug 2026).
|
|
10
|
+
*
|
|
11
|
+
* Replaces recurring synchronous git subprocess calls (git rev-parse, git status, git diff)
|
|
12
|
+
* during PreToolUse evaluation with mtime-invalidated in-memory cache lookups (<0.2ms).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const FIXED_GIT_BIN_CANDIDATES = [
|
|
16
|
+
'/usr/bin/git',
|
|
17
|
+
'/opt/homebrew/bin/git',
|
|
18
|
+
'/usr/local/bin/git',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function canExecuteAbsoluteBinary(candidate) {
|
|
22
|
+
const normalized = String(candidate || '').trim();
|
|
23
|
+
if (!normalized || !normalized.includes(path.sep)) return false;
|
|
24
|
+
try {
|
|
25
|
+
fs.accessSync(normalized, fs.constants.X_OK);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve an absolute git binary path (no PATH-only spawn).
|
|
34
|
+
* Mirrors scripts/operational-integrity.js resolveGitBinary fixed-candidate pattern.
|
|
35
|
+
* @param {Object} [options]
|
|
36
|
+
* @returns {string|null}
|
|
37
|
+
*/
|
|
38
|
+
function resolveGitBinary(options = {}) {
|
|
39
|
+
const env = options.env || process.env;
|
|
40
|
+
const candidates = Array.isArray(options.candidates) && options.candidates.length > 0
|
|
41
|
+
? options.candidates
|
|
42
|
+
: [env.THUMBGATE_GIT_BIN, ...FIXED_GIT_BIN_CANDIDATES];
|
|
43
|
+
|
|
44
|
+
for (const candidate of candidates) {
|
|
45
|
+
if (!canExecuteAbsoluteBinary(candidate)) continue;
|
|
46
|
+
return String(candidate).trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const GIT_BIN = resolveGitBinary();
|
|
53
|
+
|
|
54
|
+
class GitFastCache {
|
|
55
|
+
constructor(options = {}) {
|
|
56
|
+
this.ttlMs = options.ttlMs || 3000;
|
|
57
|
+
this.cache = new Map(); // repoRoot -> CachedRepoState
|
|
58
|
+
this.gitBin = options.gitBin || GIT_BIN;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
execGit(args, cwd) {
|
|
62
|
+
if (!this.gitBin) {
|
|
63
|
+
throw new Error('Git executable is unavailable in this runtime');
|
|
64
|
+
}
|
|
65
|
+
return execFileSync(this.gitBin, args, {
|
|
66
|
+
cwd,
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
findRepoRoot(startDir = process.cwd()) {
|
|
73
|
+
let current = path.resolve(startDir);
|
|
74
|
+
try {
|
|
75
|
+
current = fs.realpathSync(current);
|
|
76
|
+
} catch {
|
|
77
|
+
// fallback
|
|
78
|
+
}
|
|
79
|
+
while (current !== path.dirname(current)) {
|
|
80
|
+
const gitDir = path.join(current, '.git');
|
|
81
|
+
if (fs.existsSync(gitDir)) {
|
|
82
|
+
return current;
|
|
83
|
+
}
|
|
84
|
+
current = path.dirname(current);
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getGitMetaDir(repoRoot) {
|
|
90
|
+
const gitPath = path.join(repoRoot, '.git');
|
|
91
|
+
if (!fs.existsSync(gitPath)) return null;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const stat = fs.statSync(gitPath);
|
|
95
|
+
if (stat.isDirectory()) return fs.realpathSync(gitPath);
|
|
96
|
+
if (stat.isFile()) {
|
|
97
|
+
// Handle git worktree pointer file: "gitdir: /path/to/worktree"
|
|
98
|
+
const content = fs.readFileSync(gitPath, 'utf8').trim();
|
|
99
|
+
const match = /^gitdir:\s*(.+)$/i.exec(content);
|
|
100
|
+
if (match?.[1]) {
|
|
101
|
+
return fs.realpathSync(path.resolve(repoRoot, match[1].trim()));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getCacheFingerprint(gitDir) {
|
|
111
|
+
if (!gitDir || !fs.existsSync(gitDir)) return 'empty';
|
|
112
|
+
try {
|
|
113
|
+
const headFile = path.join(gitDir, 'HEAD');
|
|
114
|
+
const indexFile = path.join(gitDir, 'index');
|
|
115
|
+
const headMtime = fs.existsSync(headFile) ? fs.statSync(headFile).mtimeMs : 0;
|
|
116
|
+
const indexMtime = fs.existsSync(indexFile) ? fs.statSync(indexFile).mtimeMs : 0;
|
|
117
|
+
return `${headMtime}:${indexMtime}`;
|
|
118
|
+
} catch {
|
|
119
|
+
return 'unknown';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getRepoState(targetDir = process.cwd()) {
|
|
124
|
+
const repoRoot = this.findRepoRoot(targetDir);
|
|
125
|
+
if (!repoRoot) {
|
|
126
|
+
return {
|
|
127
|
+
isGitRepo: false,
|
|
128
|
+
repoRoot: null,
|
|
129
|
+
branch: null,
|
|
130
|
+
headSha: null,
|
|
131
|
+
isDirty: false,
|
|
132
|
+
stagedFiles: [],
|
|
133
|
+
lookupTimeMs: 0,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const start = process.hrtime.bigint();
|
|
138
|
+
const gitDir = this.getGitMetaDir(repoRoot);
|
|
139
|
+
const fingerprint = this.getCacheFingerprint(gitDir);
|
|
140
|
+
const now = Date.now();
|
|
141
|
+
|
|
142
|
+
const cached = this.cache.get(repoRoot);
|
|
143
|
+
if (cached && cached.fingerprint === fingerprint && (now - cached.cachedAt) < this.ttlMs) {
|
|
144
|
+
const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
|
|
145
|
+
return { ...cached.state, lookupTimeMs: durationMs, cached: true };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Refresh state from git inspection
|
|
149
|
+
let branch = 'unknown';
|
|
150
|
+
let headSha = null;
|
|
151
|
+
|
|
152
|
+
if (gitDir) {
|
|
153
|
+
try {
|
|
154
|
+
const headContent = fs.readFileSync(path.join(gitDir, 'HEAD'), 'utf8').trim();
|
|
155
|
+
if (headContent.startsWith('ref: refs/heads/')) {
|
|
156
|
+
branch = headContent.replace('ref: refs/heads/', '');
|
|
157
|
+
const refFile = path.join(gitDir, 'refs', 'heads', branch);
|
|
158
|
+
if (fs.existsSync(refFile)) {
|
|
159
|
+
headSha = fs.readFileSync(refFile, 'utf8').trim();
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
headSha = headContent;
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
// fallback
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!headSha) {
|
|
170
|
+
try {
|
|
171
|
+
headSha = this.execGit(['rev-parse', 'HEAD'], repoRoot).trim();
|
|
172
|
+
} catch {
|
|
173
|
+
headSha = null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let stagedFiles = [];
|
|
178
|
+
let isDirty = false;
|
|
179
|
+
try {
|
|
180
|
+
const stagedOut = this.execGit(['diff', '--cached', '--name-only'], repoRoot);
|
|
181
|
+
stagedFiles = stagedOut.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
182
|
+
const statusOut = this.execGit(['status', '--porcelain'], repoRoot);
|
|
183
|
+
isDirty = statusOut.trim().length > 0;
|
|
184
|
+
} catch {
|
|
185
|
+
// fallback
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const state = {
|
|
189
|
+
isGitRepo: true,
|
|
190
|
+
repoRoot,
|
|
191
|
+
branch,
|
|
192
|
+
headSha,
|
|
193
|
+
isDirty,
|
|
194
|
+
stagedFiles,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const finalFingerprint = this.getCacheFingerprint(gitDir);
|
|
198
|
+
this.cache.set(repoRoot, {
|
|
199
|
+
fingerprint: finalFingerprint,
|
|
200
|
+
cachedAt: now,
|
|
201
|
+
state,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
|
|
205
|
+
return { ...state, lookupTimeMs: durationMs, cached: false };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
clear() {
|
|
209
|
+
this.cache.clear();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const defaultCache = new GitFastCache();
|
|
214
|
+
|
|
215
|
+
module.exports = {
|
|
216
|
+
GitFastCache,
|
|
217
|
+
defaultCache,
|
|
218
|
+
resolveGitBinary,
|
|
219
|
+
FIXED_GIT_BIN_CANDIDATES,
|
|
220
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write-Ahead Log (WAL) & Optimistic Concurrency Engine for Git Mutations
|
|
3
|
+
*
|
|
4
|
+
* Inspired by Cursor Continuity's WAL-first architecture (https://cursor.com/blog/git-at-any-scale).
|
|
5
|
+
* Provides an append-only, tamper-evident WAL and atomic Compare-And-Swap (CAS) index
|
|
6
|
+
* to prevent multi-agent race conditions during file mutations and tool calls.
|
|
7
|
+
*
|
|
8
|
+
* @module git-wal-sync
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
const crypto = require('node:crypto');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Computes a SHA-256 digest of mutation payload.
|
|
17
|
+
*
|
|
18
|
+
* @param {object} payload - Mutation data
|
|
19
|
+
* @returns {string} Hex SHA-256
|
|
20
|
+
*/
|
|
21
|
+
function computeChecksum(payload) {
|
|
22
|
+
return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Initializes a WAL session directory for a repository.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} repoRoot - Path to repository root
|
|
29
|
+
* @returns {object} WAL session metadata
|
|
30
|
+
*/
|
|
31
|
+
function initWalSession(repoRoot) {
|
|
32
|
+
const walDir = path.join(repoRoot, '.thumbgate', 'wal');
|
|
33
|
+
fs.mkdirSync(walDir, { recursive: true });
|
|
34
|
+
|
|
35
|
+
const indexPath = path.join(walDir, 'gitwal-index.json');
|
|
36
|
+
let index = { seq: 0, etag: 'e0', lastUpdated: Date.now() };
|
|
37
|
+
|
|
38
|
+
if (fs.existsSync(indexPath)) {
|
|
39
|
+
try {
|
|
40
|
+
index = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
|
41
|
+
} catch {
|
|
42
|
+
index = { seq: 0, etag: 'e0', lastUpdated: Date.now() };
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), 'utf8');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
repoRoot,
|
|
50
|
+
walDir,
|
|
51
|
+
indexPath,
|
|
52
|
+
index,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Appends an immutable WAL entry to the log.
|
|
58
|
+
*
|
|
59
|
+
* @param {object} walSession - Active WAL session
|
|
60
|
+
* @param {object} mutation - Proposed tool mutation (toolName, filePath, diff, etc.)
|
|
61
|
+
* @returns {object} Appended entry with assigned sequence number and checksum
|
|
62
|
+
*/
|
|
63
|
+
function appendWalEntry(walSession, mutation) {
|
|
64
|
+
const seq = (walSession.index.seq || 0) + 1;
|
|
65
|
+
const entry = {
|
|
66
|
+
seq,
|
|
67
|
+
timestamp: Date.now(),
|
|
68
|
+
toolName: mutation.toolName || 'unknown',
|
|
69
|
+
filePath: mutation.filePath || null,
|
|
70
|
+
operation: mutation.operation || 'modify',
|
|
71
|
+
payloadChecksum: computeChecksum(mutation),
|
|
72
|
+
actor: mutation.actor || process.env.THUMBGATE_SESSION_AGENT || 'local-agent',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const entryFile = path.join(walSession.walDir, `entry-${String(seq).padStart(6, '0')}.wal.json`);
|
|
76
|
+
fs.writeFileSync(entryFile, JSON.stringify(entry, null, 2), 'utf8');
|
|
77
|
+
|
|
78
|
+
return entry;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Performs an atomic Compare-And-Swap (CAS) commit of the WAL index.
|
|
83
|
+
*
|
|
84
|
+
* @param {object} walSession - Active WAL session
|
|
85
|
+
* @param {number} expectedSeq - Expected prior sequence number
|
|
86
|
+
* @param {object} newEntry - The newly appended WAL entry
|
|
87
|
+
* @returns {{ ok: boolean, currentSeq: number, etag: string, error?: string }}
|
|
88
|
+
*/
|
|
89
|
+
function casCommitIndex(walSession, expectedSeq, newEntry) {
|
|
90
|
+
let currentIndex = { seq: 0, etag: 'e0' };
|
|
91
|
+
if (fs.existsSync(walSession.indexPath)) {
|
|
92
|
+
try {
|
|
93
|
+
currentIndex = JSON.parse(fs.readFileSync(walSession.indexPath, 'utf8'));
|
|
94
|
+
} catch {
|
|
95
|
+
currentIndex = { seq: 0, etag: 'e0' };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (currentIndex.seq !== expectedSeq) {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
currentSeq: currentIndex.seq,
|
|
103
|
+
etag: currentIndex.etag,
|
|
104
|
+
error: `CAS Precondition Failed: expected seq ${expectedSeq} but found ${currentIndex.seq}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const updatedIndex = {
|
|
109
|
+
seq: newEntry.seq,
|
|
110
|
+
etag: `e${newEntry.seq}`,
|
|
111
|
+
lastUpdated: Date.now(),
|
|
112
|
+
lastChecksum: newEntry.payloadChecksum,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
fs.writeFileSync(walSession.indexPath, JSON.stringify(updatedIndex, null, 2), 'utf8');
|
|
116
|
+
walSession.index = updatedIndex;
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
ok: true,
|
|
120
|
+
currentSeq: updatedIndex.seq,
|
|
121
|
+
etag: updatedIndex.etag,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Reads all WAL entries up to the current sequence number.
|
|
127
|
+
*
|
|
128
|
+
* @param {object} walSession - Active WAL session
|
|
129
|
+
* @returns {object[]} List of valid WAL entries
|
|
130
|
+
*/
|
|
131
|
+
function readWalEntries(walSession) {
|
|
132
|
+
if (!fs.existsSync(walSession.walDir)) return [];
|
|
133
|
+
|
|
134
|
+
const files = fs.readdirSync(walSession.walDir)
|
|
135
|
+
.filter((f) => f.startsWith('entry-') && f.endsWith('.wal.json'))
|
|
136
|
+
.sort((a, b) => a.localeCompare(b));
|
|
137
|
+
|
|
138
|
+
const entries = [];
|
|
139
|
+
for (const f of files) {
|
|
140
|
+
try {
|
|
141
|
+
const content = fs.readFileSync(path.join(walSession.walDir, f), 'utf8');
|
|
142
|
+
entries.push(JSON.parse(content));
|
|
143
|
+
} catch {
|
|
144
|
+
// skip corrupted entries
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return entries;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
computeChecksum,
|
|
152
|
+
initWalSession,
|
|
153
|
+
appendWalEntry,
|
|
154
|
+
casCommitIndex,
|
|
155
|
+
readWalEntries,
|
|
156
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* HashAnchoredEditGuard — Pre-edit concurrency & content collision guard.
|
|
8
|
+
* Inspired by Dirac's Hash-Anchored Edit Engine (Aug 2026).
|
|
9
|
+
*
|
|
10
|
+
* Prevents race condition regressions where concurrent agent sessions or human operators
|
|
11
|
+
* modify code lines while an agent is generating an edit.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function computeChunkHash(content) {
|
|
15
|
+
return crypto.createHash('sha256').update(String(content || '')).digest('hex').slice(0, 16);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function evaluateHashAnchoredEdit(request = {}) {
|
|
19
|
+
const { filePath, targetContent, expectedHash, startLine, endLine } = request;
|
|
20
|
+
|
|
21
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
22
|
+
return {
|
|
23
|
+
allowed: false,
|
|
24
|
+
reason: 'target_file_not_found',
|
|
25
|
+
message: `File does not exist: ${filePath}`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let fileContent = '';
|
|
30
|
+
try {
|
|
31
|
+
fileContent = fs.readFileSync(filePath, 'utf8');
|
|
32
|
+
} catch (err) {
|
|
33
|
+
return {
|
|
34
|
+
allowed: false,
|
|
35
|
+
reason: 'file_read_error',
|
|
36
|
+
message: `Failed to read ${filePath}: ${err.message}`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const lines = fileContent.split('\n');
|
|
41
|
+
let currentSlice = '';
|
|
42
|
+
|
|
43
|
+
if (typeof startLine === 'number' && typeof endLine === 'number' && startLine >= 1 && endLine <= lines.length) {
|
|
44
|
+
currentSlice = lines.slice(startLine - 1, endLine).join('\n');
|
|
45
|
+
} else if (targetContent) {
|
|
46
|
+
// Exact target content search
|
|
47
|
+
if (!fileContent.includes(targetContent)) {
|
|
48
|
+
return {
|
|
49
|
+
allowed: false,
|
|
50
|
+
reason: 'target_content_drift',
|
|
51
|
+
message: `Target code block was modified or does not match current file contents.`,
|
|
52
|
+
currentFileHash: computeChunkHash(fileContent),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
currentSlice = targetContent;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const actualHash = computeChunkHash(currentSlice);
|
|
59
|
+
|
|
60
|
+
if (expectedHash && expectedHash !== actualHash) {
|
|
61
|
+
return {
|
|
62
|
+
allowed: false,
|
|
63
|
+
reason: 'hash_anchor_mismatch',
|
|
64
|
+
message: `Hash anchor drift detected: expected ${expectedHash}, found ${actualHash}. Another session or user modified this region.`,
|
|
65
|
+
expectedHash,
|
|
66
|
+
actualHash,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
allowed: true,
|
|
72
|
+
reason: 'anchor_verified',
|
|
73
|
+
chunkHash: actualHash,
|
|
74
|
+
lineCount: lines.length,
|
|
75
|
+
timestamp: new Date().toISOString(),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
computeChunkHash,
|
|
81
|
+
evaluateHashAnchoredEdit,
|
|
82
|
+
};
|