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,284 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hidden-entry-points scorecard — misconfigs attackers / runaway agents rely on.
|
|
6
|
+
*
|
|
7
|
+
* Process steal from a BrightTALK weekly rec digest (not BrightTALK, SailPoint,
|
|
8
|
+
* Strike48, Abnormal, A-LIGN, or ISO 42001 product):
|
|
9
|
+
* - "Watch everything" is not the digest — rank by interest, drop vendor theater
|
|
10
|
+
* - Hidden entry points = PreToolUse / identity / dynamic-tool misconfigs
|
|
11
|
+
* - Going agentic without a pre-action gate is the SOC pitfall
|
|
12
|
+
*
|
|
13
|
+
* Existing surfaces: agent-security-central hook drift + claw evaluateClawPretool.
|
|
14
|
+
* Not a new SOC / ISO 42001 SKU. Not BrightTALK affiliation.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* node scripts/hidden-entry-points.js
|
|
18
|
+
* node scripts/hidden-entry-points.js --json
|
|
19
|
+
* npm run hidden:entry
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('node:fs');
|
|
23
|
+
const path = require('node:path');
|
|
24
|
+
|
|
25
|
+
const SCHEMA = 'thumbgate.hidden_entry_points.v1';
|
|
26
|
+
const DEFAULT_INTERESTS = ['hidden-entry', 'identity', 'agentic-soc', 'ai-security'];
|
|
27
|
+
const DEFAULT_MAX_DIGEST = 5;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Fixture set: load-all is "watch every webinar"; digest is interest-ranked.
|
|
31
|
+
* `iso42001-theater` and vendor webinars are noise (not attacker entry points).
|
|
32
|
+
*/
|
|
33
|
+
const ENTRY_POINTS = [
|
|
34
|
+
{
|
|
35
|
+
id: 'pretooluse-unwired',
|
|
36
|
+
theme: 'hidden-entry',
|
|
37
|
+
label: 'PreToolUse hook not on the wire',
|
|
38
|
+
attackerReliesOn: true,
|
|
39
|
+
noise: false,
|
|
40
|
+
open: true,
|
|
41
|
+
severity: 9,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'dynamic-tool-ungated',
|
|
45
|
+
theme: 'agentic-soc',
|
|
46
|
+
label: 'Runtime dynamic tool creation without approval',
|
|
47
|
+
attackerReliesOn: true,
|
|
48
|
+
noise: false,
|
|
49
|
+
open: true,
|
|
50
|
+
severity: 8,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'computer-use-ungated',
|
|
54
|
+
theme: 'hidden-entry',
|
|
55
|
+
label: 'Screen / computer-use without a pre-action gate',
|
|
56
|
+
attackerReliesOn: true,
|
|
57
|
+
noise: false,
|
|
58
|
+
open: true,
|
|
59
|
+
severity: 8,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'agent-identity-missing',
|
|
63
|
+
theme: 'identity',
|
|
64
|
+
label: 'Tool calls without agent_identity',
|
|
65
|
+
attackerReliesOn: true,
|
|
66
|
+
noise: false,
|
|
67
|
+
open: true,
|
|
68
|
+
severity: 7,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'review-volume-as-control',
|
|
72
|
+
theme: 'agentic-soc',
|
|
73
|
+
label: 'Human review volume treated as the control',
|
|
74
|
+
attackerReliesOn: true,
|
|
75
|
+
noise: false,
|
|
76
|
+
open: true,
|
|
77
|
+
severity: 6,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'iso42001-theater',
|
|
81
|
+
theme: 'compliance',
|
|
82
|
+
label: 'Treat ISO 42001 marketing as an attestation',
|
|
83
|
+
attackerReliesOn: false,
|
|
84
|
+
noise: true,
|
|
85
|
+
open: false,
|
|
86
|
+
severity: 1,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'vendor-webinar-sailpoint',
|
|
90
|
+
theme: 'identity',
|
|
91
|
+
label: 'SailPoint / BrightTALK vendor webinar as a product claim',
|
|
92
|
+
attackerReliesOn: false,
|
|
93
|
+
noise: true,
|
|
94
|
+
open: false,
|
|
95
|
+
severity: 0,
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
function rankDigest(entries, options = {}) {
|
|
100
|
+
const interests = Array.isArray(options.interests) && options.interests.length
|
|
101
|
+
? options.interests
|
|
102
|
+
: DEFAULT_INTERESTS;
|
|
103
|
+
const maxDigest = Number.isFinite(options.maxDigest) && options.maxDigest > 0
|
|
104
|
+
? options.maxDigest
|
|
105
|
+
: DEFAULT_MAX_DIGEST;
|
|
106
|
+
const interestSet = new Set(interests);
|
|
107
|
+
return [...entries]
|
|
108
|
+
.filter((e) => !e.noise && interestSet.has(e.theme))
|
|
109
|
+
.sort((a, b) => (b.severity || 0) - (a.severity || 0))
|
|
110
|
+
.slice(0, maxDigest);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function applyLiveHookDrift(entries, projectRoot, homeDir, assessHookDriftFn) {
|
|
114
|
+
if (!projectRoot) return { entries, error: null };
|
|
115
|
+
try {
|
|
116
|
+
const assessHookDrift = typeof assessHookDriftFn === 'function'
|
|
117
|
+
? assessHookDriftFn
|
|
118
|
+
: require('./agent-security-central').assessHookDrift;
|
|
119
|
+
const assessment = assessHookDrift(projectRoot, homeDir);
|
|
120
|
+
const drifted = Boolean(assessment && assessment.drifted);
|
|
121
|
+
return {
|
|
122
|
+
entries: entries.map((e) => (
|
|
123
|
+
e.id === 'pretooluse-unwired' ? { ...e, open: drifted } : e
|
|
124
|
+
)),
|
|
125
|
+
error: null,
|
|
126
|
+
};
|
|
127
|
+
} catch (err) {
|
|
128
|
+
return {
|
|
129
|
+
entries,
|
|
130
|
+
error: `Live hook drift assessment failed: ${err.message}`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function runHiddenEntryScorecard(options = {}) {
|
|
136
|
+
const base = Array.isArray(options.entries) && options.entries.length
|
|
137
|
+
? options.entries
|
|
138
|
+
: ENTRY_POINTS;
|
|
139
|
+
const { entries, error: liveError } = applyLiveHookDrift(
|
|
140
|
+
base,
|
|
141
|
+
options.projectRoot,
|
|
142
|
+
options.homeDir,
|
|
143
|
+
options.assessHookDrift
|
|
144
|
+
);
|
|
145
|
+
const loadAll = entries.map((e) => e.id);
|
|
146
|
+
const digest = rankDigest(entries, options);
|
|
147
|
+
const openAttacker = entries.filter((e) => e.attackerReliesOn && e.open);
|
|
148
|
+
const noiseDropped = entries.filter((e) => e.noise).map((e) => e.id);
|
|
149
|
+
const digestIds = digest.map((e) => e.id);
|
|
150
|
+
const digestHasNoise = digest.some((e) => e.noise);
|
|
151
|
+
const digestHasClosedTheater = digest.some((e) => !e.attackerReliesOn);
|
|
152
|
+
|
|
153
|
+
const failures = [];
|
|
154
|
+
if (liveError) {
|
|
155
|
+
failures.push(liveError);
|
|
156
|
+
}
|
|
157
|
+
if (openAttacker.length === 0) {
|
|
158
|
+
failures.push('no open hidden entry — fixture is not a misconfig case');
|
|
159
|
+
}
|
|
160
|
+
if (digest.length === 0) {
|
|
161
|
+
failures.push('interest digest is empty');
|
|
162
|
+
}
|
|
163
|
+
if (digest.length >= loadAll.length) {
|
|
164
|
+
failures.push('digest did not rank — load-all still shown');
|
|
165
|
+
}
|
|
166
|
+
if (digestHasNoise || digestHasClosedTheater) {
|
|
167
|
+
failures.push('digest still contains vendor/ISO theater');
|
|
168
|
+
}
|
|
169
|
+
if (!digestIds.includes('pretooluse-unwired') && openAttacker.some((e) => e.id === 'pretooluse-unwired')) {
|
|
170
|
+
failures.push('open PreToolUse miss not in digest');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let mode = 'simulation';
|
|
174
|
+
if (options.projectRoot) {
|
|
175
|
+
mode = liveError ? 'live-assessment-failed' : 'live-overlay';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
schema: SCHEMA,
|
|
180
|
+
mode,
|
|
181
|
+
generatedAt: new Date().toISOString(),
|
|
182
|
+
autoApply: false,
|
|
183
|
+
humanOversightRequired: true,
|
|
184
|
+
reviewVolumeIsNotTheControl: true,
|
|
185
|
+
capturedRevenueUsd: 0,
|
|
186
|
+
affiliation: 'none',
|
|
187
|
+
iso42001Certified: false,
|
|
188
|
+
process: {
|
|
189
|
+
source: 'BrightTALK weekly-rec process — not BrightTALK/SailPoint/ISO 42001 product',
|
|
190
|
+
interestRankedDigest: true,
|
|
191
|
+
unifiedAlertManager: false,
|
|
192
|
+
},
|
|
193
|
+
interests: options.interests || DEFAULT_INTERESTS,
|
|
194
|
+
loadAll,
|
|
195
|
+
digest: digestIds,
|
|
196
|
+
openAttacker: openAttacker.map((e) => e.id),
|
|
197
|
+
noiseDropped,
|
|
198
|
+
summary: {
|
|
199
|
+
ok: failures.length === 0,
|
|
200
|
+
failures,
|
|
201
|
+
},
|
|
202
|
+
disclaimers: [
|
|
203
|
+
'SIMULATION unless --project. Does not install hooks.',
|
|
204
|
+
'Not BrightTALK, SailPoint, Strike48, Abnormal, A-LIGN, or ISO 42001.',
|
|
205
|
+
'Review volume is not the control — PreToolUse is. capturedRevenueUsd is 0.',
|
|
206
|
+
'ThumbGate is not ISO 42001 certified. Do not treat this scorecard as an attestation.',
|
|
207
|
+
],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function formatReport(report) {
|
|
212
|
+
return [
|
|
213
|
+
'# Hidden entry points vs interest digest',
|
|
214
|
+
'',
|
|
215
|
+
`Result: ${report.summary.ok ? 'PASS' : 'FAIL'} open=${(report.openAttacker || []).length}`,
|
|
216
|
+
'',
|
|
217
|
+
`| Path | Count |`,
|
|
218
|
+
`|------|------:|`,
|
|
219
|
+
`| Load-all webinars | ${report.loadAll.length} |`,
|
|
220
|
+
`| Interest digest | ${report.digest.length} |`,
|
|
221
|
+
`| Open attacker entries | ${report.openAttacker.length} |`,
|
|
222
|
+
'',
|
|
223
|
+
`Digest: ${(report.digest || []).join(', ') || '—'}`,
|
|
224
|
+
`Noise dropped: ${(report.noiseDropped || []).join(', ') || '—'}`,
|
|
225
|
+
`iso42001Certified=${report.iso42001Certified} autoApply=${report.autoApply} capturedRevenueUsd=${report.capturedRevenueUsd}`,
|
|
226
|
+
'',
|
|
227
|
+
...report.disclaimers.map((d) => `- ${d}`),
|
|
228
|
+
'',
|
|
229
|
+
].join('\n');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function mainCli(argv = process.argv.slice(2)) {
|
|
233
|
+
const json = argv.includes('--json');
|
|
234
|
+
const writeIdx = argv.indexOf('--write');
|
|
235
|
+
let writePath = null;
|
|
236
|
+
if (writeIdx >= 0) {
|
|
237
|
+
const candidate = argv[writeIdx + 1];
|
|
238
|
+
if (!candidate || candidate.startsWith('-')) {
|
|
239
|
+
process.stderr.write('Error: --write requires a valid file path\n');
|
|
240
|
+
return 1;
|
|
241
|
+
}
|
|
242
|
+
writePath = candidate;
|
|
243
|
+
}
|
|
244
|
+
const projectIdx = argv.indexOf('--project');
|
|
245
|
+
let projectRoot = null;
|
|
246
|
+
if (projectIdx >= 0) {
|
|
247
|
+
const candidate = argv[projectIdx + 1];
|
|
248
|
+
if (!candidate || candidate.startsWith('-')) {
|
|
249
|
+
process.stderr.write('Error: --project requires a valid directory path\n');
|
|
250
|
+
return 1;
|
|
251
|
+
}
|
|
252
|
+
projectRoot = candidate;
|
|
253
|
+
}
|
|
254
|
+
const report = runHiddenEntryScorecard(projectRoot ? { projectRoot } : {});
|
|
255
|
+
|
|
256
|
+
if (writePath) {
|
|
257
|
+
const abs = path.resolve(writePath);
|
|
258
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
259
|
+
const jsonPath = /\.json$/i.test(abs) ? abs : `${abs}.json`;
|
|
260
|
+
fs.writeFileSync(jsonPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (json) {
|
|
264
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
265
|
+
} else {
|
|
266
|
+
process.stdout.write(formatReport(report));
|
|
267
|
+
}
|
|
268
|
+
return report.summary.ok ? 0 : 1;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
module.exports = {
|
|
272
|
+
SCHEMA,
|
|
273
|
+
DEFAULT_INTERESTS,
|
|
274
|
+
ENTRY_POINTS,
|
|
275
|
+
rankDigest,
|
|
276
|
+
applyLiveHookDrift,
|
|
277
|
+
runHiddenEntryScorecard,
|
|
278
|
+
formatReport,
|
|
279
|
+
mainCli,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
if (path.resolve(process.argv[1] || '') === path.resolve(__filename)) {
|
|
283
|
+
process.exitCode = mainCli();
|
|
284
|
+
}
|
|
@@ -49,6 +49,7 @@ function requestEscalation(input = {}, options = {}) {
|
|
|
49
49
|
const ttlMs = Math.min(MAX_TTL_MS, Math.max(1, finiteNumber(input.ttlMs, DEFAULT_TTL_MS)));
|
|
50
50
|
const idempotencyKey = requiredString(input.idempotencyKey || taskId, 'idempotencyKey');
|
|
51
51
|
const approvalContextDigest = optionalDigest(input.approvalContextDigest, 'approvalContextDigest');
|
|
52
|
+
const requiredReviewerRole = optionalString(input.requiredReviewerRole);
|
|
52
53
|
const request = {
|
|
53
54
|
escalationId: input.escalationId || `esc_${crypto.randomUUID()}`,
|
|
54
55
|
idempotencyKey,
|
|
@@ -63,6 +64,7 @@ function requestEscalation(input = {}, options = {}) {
|
|
|
63
64
|
eventType: 'requested',
|
|
64
65
|
};
|
|
65
66
|
if (approvalContextDigest) request.approvalContextDigest = approvalContextDigest;
|
|
67
|
+
if (requiredReviewerRole) request.requiredReviewerRole = requiredReviewerRole;
|
|
66
68
|
return withEscalationLock(options, () => {
|
|
67
69
|
const ledger = readLedger(options);
|
|
68
70
|
assertLedgerHealthy(ledger);
|
|
@@ -107,6 +109,9 @@ function decideEscalation(input = {}, options = {}) {
|
|
|
107
109
|
if (!current) throw escalationError(`unknown escalation '${escalationId}'`);
|
|
108
110
|
if (current.status !== 'pending') throw escalationError(`escalation '${escalationId}' is already ${current.status}`);
|
|
109
111
|
if (sameIdentity(current.requester, actor)) throw escalationError('requester cannot decide their own escalation');
|
|
112
|
+
if (current.requiredReviewerRole && actor.role !== current.requiredReviewerRole) {
|
|
113
|
+
throw escalationError(`authenticated reviewer must have role '${current.requiredReviewerRole}'`);
|
|
114
|
+
}
|
|
110
115
|
|
|
111
116
|
const now = options.now || new Date();
|
|
112
117
|
const event = {
|
|
@@ -145,6 +150,8 @@ function getVerifiedApprovalUnlocked(escalationId, options = {}) {
|
|
|
145
150
|
const events = ledger.events.filter((event) => event.escalationId === escalationId);
|
|
146
151
|
const requested = events.find((event) => event.eventType === 'requested');
|
|
147
152
|
const decided = events.findLast((event) => event.eventType === 'decided');
|
|
153
|
+
const consumed = events.findLast((event) => event.eventType === 'consumed');
|
|
154
|
+
if (consumed && options.allowConsumed !== true) return null;
|
|
148
155
|
if (!requested || !decided || decided.status !== 'approved') return null;
|
|
149
156
|
if (decided.taskId !== requested.taskId) throw escalationError('approval task does not match its request');
|
|
150
157
|
if ((requested.approvalContextDigest || null) !== (decided.approvalContextDigest || null)) {
|
|
@@ -153,6 +160,9 @@ function getVerifiedApprovalUnlocked(escalationId, options = {}) {
|
|
|
153
160
|
if (decided.actor?.kind !== 'human' || sameIdentity(requested.requester, decided.actor)) {
|
|
154
161
|
throw escalationError('approval is not from an independent human actor');
|
|
155
162
|
}
|
|
163
|
+
if (requested.requiredReviewerRole && decided.actor.role !== requested.requiredReviewerRole) {
|
|
164
|
+
throw escalationError('approval is not from the required reviewer role');
|
|
165
|
+
}
|
|
156
166
|
const verificationKey = optionalString(
|
|
157
167
|
options.approvalVerificationKey || process.env.THUMBGATE_HUMAN_REVIEWER_KEY
|
|
158
168
|
);
|
|
@@ -162,6 +172,33 @@ function getVerifiedApprovalUnlocked(escalationId, options = {}) {
|
|
|
162
172
|
return { ...requested, ...decided };
|
|
163
173
|
}
|
|
164
174
|
|
|
175
|
+
function consumeVerifiedApproval(escalationId, input = {}, options = {}) {
|
|
176
|
+
const consumer = requiredIdentity(input.consumer, 'consumer');
|
|
177
|
+
return withEscalationLock(options, () => {
|
|
178
|
+
const ledger = readLedger(options);
|
|
179
|
+
assertLedgerHealthy(ledger);
|
|
180
|
+
const events = ledger.events.filter((event) => event.escalationId === escalationId);
|
|
181
|
+
const existingConsumption = events.findLast((event) => event.eventType === 'consumed');
|
|
182
|
+
if (existingConsumption) {
|
|
183
|
+
return { consumed: false, replayed: true, consumption: existingConsumption };
|
|
184
|
+
}
|
|
185
|
+
const approval = getVerifiedApprovalUnlocked(escalationId, { ...options, allowConsumed: true });
|
|
186
|
+
if (!approval || Date.parse(approval.expiresAt) <= (options.now || new Date()).getTime()) {
|
|
187
|
+
return { consumed: false, replayed: false, consumption: null };
|
|
188
|
+
}
|
|
189
|
+
const consumedAt = (options.now || new Date()).toISOString();
|
|
190
|
+
const consumption = appendEventUnlocked({
|
|
191
|
+
escalationId,
|
|
192
|
+
taskId: approval.taskId,
|
|
193
|
+
eventType: 'consumed',
|
|
194
|
+
consumer,
|
|
195
|
+
consumedAt,
|
|
196
|
+
approvalContextDigest: approval.approvalContextDigest || null,
|
|
197
|
+
}, options, ledger.events);
|
|
198
|
+
return { consumed: true, replayed: false, approval, consumption };
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
165
202
|
function listEscalations(options = {}) {
|
|
166
203
|
return projectEscalations(readEvents(options), options);
|
|
167
204
|
}
|
|
@@ -454,7 +491,7 @@ function approvalPayload(event) {
|
|
|
454
491
|
taskId: event.taskId,
|
|
455
492
|
status: event.status,
|
|
456
493
|
decision: event.decision,
|
|
457
|
-
actor: event.actor,
|
|
494
|
+
actor: event.actor ? { ...event.actor } : event.actor,
|
|
458
495
|
reason: event.reason,
|
|
459
496
|
decidedAt: event.decidedAt,
|
|
460
497
|
};
|
|
@@ -470,6 +507,8 @@ function requiredIdentity(value, field) {
|
|
|
470
507
|
};
|
|
471
508
|
const displayName = optionalString(value.displayName);
|
|
472
509
|
if (displayName) identity.displayName = displayName;
|
|
510
|
+
const role = optionalString(value.role);
|
|
511
|
+
if (role) identity.role = role;
|
|
473
512
|
return identity;
|
|
474
513
|
}
|
|
475
514
|
|
|
@@ -521,6 +560,7 @@ function eventComparableHash(event) {
|
|
|
521
560
|
evidence: event.evidence,
|
|
522
561
|
};
|
|
523
562
|
if (event.approvalContextDigest) comparable.approvalContextDigest = event.approvalContextDigest;
|
|
563
|
+
if (event.requiredReviewerRole) comparable.requiredReviewerRole = event.requiredReviewerRole;
|
|
524
564
|
return crypto.createHash('sha256').update(stableStringify(comparable)).digest('hex');
|
|
525
565
|
}
|
|
526
566
|
|
|
@@ -548,6 +588,160 @@ function escalationError(message) {
|
|
|
548
588
|
return error;
|
|
549
589
|
}
|
|
550
590
|
|
|
591
|
+
function createMemoryGrantStore() {
|
|
592
|
+
const consumed = new Set();
|
|
593
|
+
return {
|
|
594
|
+
consume(grantId) {
|
|
595
|
+
const id = String(grantId || '').trim();
|
|
596
|
+
if (!id) return { ok: false, reason: 'missing_grant_id' };
|
|
597
|
+
if (consumed.has(id)) return { ok: false, reason: 'grant_already_consumed' };
|
|
598
|
+
consumed.add(id);
|
|
599
|
+
return { ok: true };
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function createDurableGrantStore(options = {}) {
|
|
605
|
+
const storePath = options.grantStorePath || path.join(
|
|
606
|
+
getFeedbackPaths(options).FEEDBACK_DIR,
|
|
607
|
+
'harness-grants.jsonl'
|
|
608
|
+
);
|
|
609
|
+
return {
|
|
610
|
+
consume(grantId) {
|
|
611
|
+
const id = String(grantId || '').trim();
|
|
612
|
+
if (!id) return { ok: false, reason: 'missing_grant_id' };
|
|
613
|
+
return withFileLedgerLock(`${storePath}.lock`, () => {
|
|
614
|
+
let raw = '';
|
|
615
|
+
try {
|
|
616
|
+
raw = fs.readFileSync(storePath, 'utf8');
|
|
617
|
+
} catch (error) {
|
|
618
|
+
if (error.code !== 'ENOENT') throw error;
|
|
619
|
+
}
|
|
620
|
+
for (const line of raw.split('\n')) {
|
|
621
|
+
if (!line.trim()) continue;
|
|
622
|
+
try {
|
|
623
|
+
if (JSON.parse(line).grantId === id) {
|
|
624
|
+
return { ok: false, reason: 'grant_already_consumed' };
|
|
625
|
+
}
|
|
626
|
+
} catch {
|
|
627
|
+
return { ok: false, reason: 'grant_store_corrupt' };
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
fs.mkdirSync(path.dirname(storePath), { recursive: true });
|
|
631
|
+
fs.appendFileSync(storePath, `${JSON.stringify({
|
|
632
|
+
grantId: id,
|
|
633
|
+
consumedAt: (options.now || new Date()).toISOString(),
|
|
634
|
+
})}\n`);
|
|
635
|
+
return { ok: true };
|
|
636
|
+
}, {
|
|
637
|
+
now: options.now,
|
|
638
|
+
lockStaleMs: options.lockStaleMs,
|
|
639
|
+
errorFactory: (message) => escalationError(message),
|
|
640
|
+
});
|
|
641
|
+
},
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
const ALLOW_ONCE_GRANT_MODES = new Set(['once', 'allow-once', 'allowed-once', 'allow_once']);
|
|
645
|
+
const ALLOW_ALWAYS_GRANT_MODES = new Set(['always', 'allow-always', 'allow_always']);
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* DeepSeek-style harness grant.
|
|
649
|
+
* Missing or throwing/failing approver fails closed (never silent-allow).
|
|
650
|
+
* Grants are allow-once only; allow-always is rejected.
|
|
651
|
+
*/
|
|
652
|
+
function evaluateHarnessGrant(input = {}, options = {}) {
|
|
653
|
+
const grantMode = String(input.grantMode || input.mode || 'once').trim().toLowerCase();
|
|
654
|
+
if (ALLOW_ALWAYS_GRANT_MODES.has(grantMode)) {
|
|
655
|
+
return { allowed: false, failClosed: true, reason: 'allow_always_forbidden' };
|
|
656
|
+
}
|
|
657
|
+
if (!ALLOW_ONCE_GRANT_MODES.has(grantMode)) {
|
|
658
|
+
return { allowed: false, failClosed: true, reason: 'unsupported_grant_mode' };
|
|
659
|
+
}
|
|
660
|
+
if (input.approver == null) {
|
|
661
|
+
return { allowed: false, failClosed: true, reason: 'missing_approver' };
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
let verdict;
|
|
665
|
+
try {
|
|
666
|
+
verdict = typeof input.approver === 'function' ? input.approver(input.action) : input.approver;
|
|
667
|
+
} catch (error) {
|
|
668
|
+
return {
|
|
669
|
+
allowed: false,
|
|
670
|
+
failClosed: true,
|
|
671
|
+
reason: 'approver_failed',
|
|
672
|
+
detail: String(error && error.message ? error.message : error),
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
if (verdict && typeof verdict.then === 'function') {
|
|
676
|
+
return {
|
|
677
|
+
allowed: false,
|
|
678
|
+
failClosed: true,
|
|
679
|
+
reason: 'approver_failed',
|
|
680
|
+
detail: 'async approver must be resolved by caller',
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
if (!verdict || verdict.allow !== true) {
|
|
684
|
+
return {
|
|
685
|
+
allowed: false,
|
|
686
|
+
failClosed: true,
|
|
687
|
+
reason: (verdict && verdict.reason) || 'approver_denied',
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const grantStore = options.grantStore || createDurableGrantStore(options);
|
|
692
|
+
const grantId = String(input.grantId || input.actionId || '').trim();
|
|
693
|
+
if (!grantId) {
|
|
694
|
+
return { allowed: false, failClosed: true, reason: 'missing_grant_id' };
|
|
695
|
+
}
|
|
696
|
+
let consumed;
|
|
697
|
+
try {
|
|
698
|
+
consumed = grantStore.consume(grantId);
|
|
699
|
+
} catch (error) {
|
|
700
|
+
return {
|
|
701
|
+
allowed: false,
|
|
702
|
+
failClosed: true,
|
|
703
|
+
reason: 'grant_store_failed',
|
|
704
|
+
detail: String(error && error.message ? error.message : error),
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
if (!consumed || !consumed.ok) {
|
|
708
|
+
return { allowed: false, failClosed: true, reason: (consumed && consumed.reason) || 'grant_already_consumed' };
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const facts = {
|
|
712
|
+
actionId: grantId,
|
|
713
|
+
toolName: input.toolName || (input.action && input.action.toolName) || 'harness_action',
|
|
714
|
+
toolInput: (input.action && input.action.toolInput) || input.toolInput || {},
|
|
715
|
+
decision: 'allow',
|
|
716
|
+
grantMode: 'allow-once',
|
|
717
|
+
principal: input.principal || 'harness',
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
if (options.recordReceipt === false) {
|
|
721
|
+
return { allowed: true, failClosed: true, grantMode: 'allow-once', grantId, facts };
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
try {
|
|
725
|
+
const { recordReceipt } = require('./action-receipts');
|
|
726
|
+
const receipt = recordReceipt({
|
|
727
|
+
actionId: facts.actionId,
|
|
728
|
+
toolName: facts.toolName,
|
|
729
|
+
toolInput: facts.toolInput,
|
|
730
|
+
decision: facts.decision,
|
|
731
|
+
principal: facts.principal,
|
|
732
|
+
outcome: { testOutcome: 'approved-once' },
|
|
733
|
+
}, options);
|
|
734
|
+
return { allowed: true, failClosed: true, grantMode: 'allow-once', grantId, facts, receipt };
|
|
735
|
+
} catch (error) {
|
|
736
|
+
return {
|
|
737
|
+
allowed: false,
|
|
738
|
+
failClosed: true,
|
|
739
|
+
reason: 'receipt_record_failed',
|
|
740
|
+
detail: String(error && error.message ? error.message : error),
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
551
745
|
function isCliInvocation() {
|
|
552
746
|
return Boolean(process.argv[1]) && path.resolve(process.argv[1]) === __filename;
|
|
553
747
|
}
|
|
@@ -565,7 +759,11 @@ if (isCliInvocation()) {
|
|
|
565
759
|
|
|
566
760
|
module.exports = {
|
|
567
761
|
calculateEscalationMetrics,
|
|
762
|
+
consumeVerifiedApproval,
|
|
763
|
+
createDurableGrantStore,
|
|
764
|
+
createMemoryGrantStore,
|
|
568
765
|
decideEscalation,
|
|
766
|
+
evaluateHarnessGrant,
|
|
569
767
|
getEscalation,
|
|
570
768
|
getEscalationsHeadPath,
|
|
571
769
|
getEscalationsJournalPath,
|