James Read 4 місяців тому
батько
коміт
235493e471

+ 1 - 0
README.md

@@ -10,6 +10,7 @@
 [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/5050/badge)](https://bestpractices.coreinfrastructure.org/projects/5050)
 [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/5050/badge)](https://bestpractices.coreinfrastructure.org/projects/5050)
 
 
 [![Go Report Card](https://goreportcard.com/badge/github.com/Olivetin/OliveTin)](https://goreportcard.com/report/github.com/OliveTin/OliveTin)
 [![Go Report Card](https://goreportcard.com/badge/github.com/Olivetin/OliveTin)](https://goreportcard.com/report/github.com/OliveTin/OliveTin)
+[![AI Autonomy Level](https://img.shields.io/badge/AI%20Autonomy-Level%201%20of%205%20(assistance--only)-blue)](https://blog.jread.com/posts/ai-levels-of-autonomy-in-software-engineering/)
 
 
 [OliveTin 2k to 3k upgrade guide](https://docs.olivetin.app/upgrade/2k3k.html)
 [OliveTin 2k to 3k upgrade guide](https://docs.olivetin.app/upgrade/2k3k.html)
 </div>
 </div>

+ 6 - 0
SECURITY.md

@@ -31,6 +31,12 @@ Please use responsible disclosure practices when reporting a vulnerability. **Yo
 
 
 * **Option B**: Please email `contact@jread.com` for responsible disclosure. 
 * **Option B**: Please email `contact@jread.com` for responsible disclosure. 
 
 
+The following notes might be helpful when reporting a vulnerability:
+
+* OliveTin does not offer a bug bounty program.
+* GitHub usernames are how we you will be credited for discoveries reported via GitHub, if using emails we'll ask for your preferred name/handle to credit you with.
+* CVEs will be requested via GitHub Security Advisories when appropriate, but we do not guarantee that all vulnerabilities will receive CVEs, as this is determined on a case-by-case basis.
+
 ## Disclosure of how vulnerabilities were found
 ## Disclosure of how vulnerabilities were found
 
 
 It is incredibly useful to not just patch security vulnerabilities, but also to understand how they were found. If you are able to share this information, it can help us and the community to better understand potential attack vectors and improve the overall security of the project.
 It is incredibly useful to not just patch security vulnerabilities, but also to understand how they were found. If you are able to share this information, it can help us and the community to better understand potential attack vectors and improve the overall security of the project.

+ 13 - 1
frontend/js/OutputTerminal.js

@@ -1,5 +1,6 @@
 import { Terminal } from '@xterm/xterm'
 import { Terminal } from '@xterm/xterm'
 import { FitAddon } from '@xterm/addon-fit'
 import { FitAddon } from '@xterm/addon-fit'
+import { WebLinksAddon } from '@xterm/addon-web-links'
 import { Mutex } from './Mutex.js'
 import { Mutex } from './Mutex.js'
 
 
 /**
 /**
@@ -18,13 +19,24 @@ export class OutputTerminal {
   constructor (executionTrackingId) {
   constructor (executionTrackingId) {
     this.executionTrackingId = executionTrackingId
     this.executionTrackingId = executionTrackingId
     this.writeMutex = new Mutex()
     this.writeMutex = new Mutex()
+    const linkHandler = {
+      activate (event, text, _range) {
+        event.preventDefault()
+        window.open(text, '_blank')
+      }
+    }
+
     this.terminal = new Terminal({
     this.terminal = new Terminal({
-      convertEol: true
+      convertEol: true,
+      linkHandler
     })
     })
 
 
     const fitAddon = new FitAddon()
     const fitAddon = new FitAddon()
     this.terminal.loadAddon(fitAddon)
     this.terminal.loadAddon(fitAddon)
     this.terminal.fit = fitAddon
     this.terminal.fit = fitAddon
+
+    this.terminal.loadAddon(new WebLinksAddon((event, uri) => linkHandler.activate(event, uri)))
+    this.linkHandlerConfigured = true
   }
   }
 
 
   async write (out, then) {
   async write (out, then) {

+ 10 - 3
frontend/package-lock.json

@@ -15,6 +15,7 @@
 				"@hugeicons/vue": "^1.0.4",
 				"@hugeicons/vue": "^1.0.4",
 				"@vitejs/plugin-vue": "^6.0.4",
 				"@vitejs/plugin-vue": "^6.0.4",
 				"@xterm/addon-fit": "^0.11.0",
 				"@xterm/addon-fit": "^0.11.0",
+				"@xterm/addon-web-links": "^0.12.0",
 				"@xterm/xterm": "^6.0.0",
 				"@xterm/xterm": "^6.0.0",
 				"iconify-icon": "^3.0.2",
 				"iconify-icon": "^3.0.2",
 				"picocrank": "^1.14.0",
 				"picocrank": "^1.14.0",
@@ -1570,6 +1571,12 @@
 			"integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==",
 			"integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==",
 			"license": "MIT"
 			"license": "MIT"
 		},
 		},
+		"node_modules/@xterm/addon-web-links": {
+			"version": "0.12.0",
+			"resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.12.0.tgz",
+			"integrity": "sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==",
+			"license": "MIT"
+		},
 		"node_modules/@xterm/xterm": {
 		"node_modules/@xterm/xterm": {
 			"version": "6.0.0",
 			"version": "6.0.0",
 			"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz",
 			"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz",
@@ -4479,9 +4486,9 @@
 			}
 			}
 		},
 		},
 		"node_modules/minimatch": {
 		"node_modules/minimatch": {
-			"version": "3.1.2",
-			"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
-			"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+			"version": "3.1.5",
+			"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+			"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
 			"license": "ISC",
 			"license": "ISC",
 			"dependencies": {
 			"dependencies": {
 				"brace-expansion": "^1.1.7"
 				"brace-expansion": "^1.1.7"

+ 2 - 1
frontend/package.json

@@ -28,13 +28,14 @@
 		"@hugeicons/vue": "^1.0.4",
 		"@hugeicons/vue": "^1.0.4",
 		"@vitejs/plugin-vue": "^6.0.4",
 		"@vitejs/plugin-vue": "^6.0.4",
 		"@xterm/addon-fit": "^0.11.0",
 		"@xterm/addon-fit": "^0.11.0",
+		"@xterm/addon-web-links": "^0.12.0",
 		"@xterm/xterm": "^6.0.0",
 		"@xterm/xterm": "^6.0.0",
 		"iconify-icon": "^3.0.2",
 		"iconify-icon": "^3.0.2",
 		"picocrank": "^1.14.0",
 		"picocrank": "^1.14.0",
 		"standard": "^17.1.2",
 		"standard": "^17.1.2",
 		"unplugin-vue-components": "^31.0.0",
 		"unplugin-vue-components": "^31.0.0",
 		"vite": "^7.3.1",
 		"vite": "^7.3.1",
-    "vue": "^3.5.29",
+		"vue": "^3.5.29",
 		"vue-i18n": "^11.2.8",
 		"vue-i18n": "^11.2.8",
 		"vue-router": "^5.0.3"
 		"vue-router": "^5.0.3"
 	}
 	}

+ 5 - 1
frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts

@@ -210,6 +210,11 @@ export declare type EffectivePolicy = Message<"olivetin.api.v1.EffectivePolicy">
    * @generated from field: bool show_log_list = 2;
    * @generated from field: bool show_log_list = 2;
    */
    */
   showLogList: boolean;
   showLogList: boolean;
+
+  /**
+   * @generated from field: bool show_version_number = 3;
+   */
+  showVersionNumber: boolean;
 };
 };
 
 
 /**
 /**
@@ -1853,4 +1858,3 @@ export declare const OliveTinApiService: GenService<{
     output: typeof EntitySchema;
     output: typeof EntitySchema;
   },
   },
 }>;
 }>;
-

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js


+ 11 - 9
frontend/resources/vue/App.vue

@@ -31,7 +31,7 @@
             <footer title="footer" v-if="showFooter">
             <footer title="footer" v-if="showFooter">
                 <p>
                 <p>
                     <img title="application icon" :src="logoUrl" alt="OliveTin logo" style="height: 1em;" class="logo" />
                     <img title="application icon" :src="logoUrl" alt="OliveTin logo" style="height: 1em;" class="logo" />
-                    OliveTin {{ currentVersion }}
+                    OliveTin <span v-if="showVersionNumber">{{ currentVersion }}</span>
                 </p>
                 </p>
                 <p>
                 <p>
                     <span>
                     <span>
@@ -52,7 +52,7 @@
 
 
                     <span>{{ t('connected') }}</span>
                     <span>{{ t('connected') }}</span>
                 </p>
                 </p>
-                <p>
+                <p v-if="showVersionNumber">
                     <a id="available-version" href="http://olivetin.app" target="_blank" hidden>?</a>
                     <a id="available-version" href="http://olivetin.app" target="_blank" hidden>?</a>
                 </p>
                 </p>
             </footer>
             </footer>
@@ -68,7 +68,7 @@
                 </option>
                 </option>
             </select>
             </select>
             <p class="browser-languages">
             <p class="browser-languages">
-                {{ t('language-dialog.browser-languages') }}: 
+                {{ t('language-dialog.browser-languages') }}:
                 <span v-if="browserLanguages.length > 0">{{ browserLanguages.join(', ') }}</span>
                 <span v-if="browserLanguages.length > 0">{{ browserLanguages.join(', ') }}</span>
                 <span v-else>{{ t('language-dialog.not-available') }}</span>
                 <span v-else>{{ t('language-dialog.not-available') }}</span>
             </p>
             </p>
@@ -126,6 +126,7 @@ const showFooter = ref(true)
 const showNavigation = ref(true)
 const showNavigation = ref(true)
 const showLogs = ref(true)
 const showLogs = ref(true)
 const showDiagnostics = ref(true)
 const showDiagnostics = ref(true)
+const showVersionNumber = ref(true)
 const showLoginLink = ref(true)
 const showLoginLink = ref(true)
 const sectionNavigationStyle = ref('sidebar')
 const sectionNavigationStyle = ref('sidebar')
 
 
@@ -184,7 +185,7 @@ function normalizeBrowserLanguage() {
     if (navigator.languages && navigator.languages.length > 0) {
     if (navigator.languages && navigator.languages.length > 0) {
         for (const candidate of navigator.languages) {
         for (const candidate of navigator.languages) {
             const lowerCandidate = candidate.toLowerCase()
             const lowerCandidate = candidate.toLowerCase()
-            
+
             // Try exact match (case-insensitive)
             // Try exact match (case-insensitive)
             const exact = available.find(locale => locale.toLowerCase() === lowerCandidate)
             const exact = available.find(locale => locale.toLowerCase() === lowerCandidate)
             if (exact) {
             if (exact) {
@@ -223,6 +224,7 @@ function updateHeaderFromInit() {
     showNavigation.value = window.initResponse.showNavigation
     showNavigation.value = window.initResponse.showNavigation
     showLogs.value = window.initResponse.showLogList
     showLogs.value = window.initResponse.showLogList
     showDiagnostics.value = window.initResponse.showDiagnostics
     showDiagnostics.value = window.initResponse.showDiagnostics
+    showVersionNumber.value = window.initResponse.effectivePolicy?.showVersionNumber ?? true
     sectionNavigationStyle.value = window.initResponse.sectionNavigationStyle || 'sidebar'
     sectionNavigationStyle.value = window.initResponse.sectionNavigationStyle || 'sidebar'
     availableThemes.value = window.initResponse.availableThemes || []
     availableThemes.value = window.initResponse.availableThemes || []
 
 
@@ -277,7 +279,7 @@ function renderNavigation() {
 
 
 function openLanguageDialog() {
 function openLanguageDialog() {
     selectedLanguage.value = languagePreference.value
     selectedLanguage.value = languagePreference.value
-    
+
     if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
     if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
         browserLanguages.value = navigator.languages
         browserLanguages.value = navigator.languages
     } else {
     } else {
@@ -327,7 +329,7 @@ function handleLanguageDialogClick(event) {
 
 
 function openThemeDialog() {
 function openThemeDialog() {
     selectedTheme.value = themePreference.value || ''
     selectedTheme.value = themePreference.value || ''
-    
+
     if (themeDialog.value) {
     if (themeDialog.value) {
         themeDialog.value.showModal()
         themeDialog.value.showModal()
     }
     }
@@ -354,7 +356,7 @@ function changeTheme() {
 
 
 function applyTheme() {
 function applyTheme() {
     let themeStyle = document.getElementById('theme-style')
     let themeStyle = document.getElementById('theme-style')
-    
+
     if (!themeStyle) {
     if (!themeStyle) {
         themeStyle = document.createElement('style')
         themeStyle = document.createElement('style')
         themeStyle.id = 'theme-style'
         themeStyle.id = 'theme-style'
@@ -404,10 +406,10 @@ window.updateHeaderFromInit = updateHeaderFromInit
 onMounted(() => {
 onMounted(() => {
     serverConnection.value = true;
     serverConnection.value = true;
     updateHeaderFromInit()
     updateHeaderFromInit()
-    
+
     // Initialize selected language from stored preference
     // Initialize selected language from stored preference
     selectedLanguage.value = languagePreference.value
     selectedLanguage.value = languagePreference.value
-    
+
     // Initialize selected theme from stored preference
     // Initialize selected theme from stored preference
     selectedTheme.value = themePreference.value || ''
     selectedTheme.value = themePreference.value || ''
 
 

+ 5 - 2
frontend/resources/vue/views/DiagnosticsView.vue

@@ -162,7 +162,10 @@ async function generateBrowserInfo() {
       userAgentData: userAgentData
       userAgentData: userAgentData
     }
     }
 
 
-    const olivetinVersion = window.initResponse?.currentVersion || t('diagnostics.unknown')
+    const showVersionNumber = window.initResponse?.effectivePolicy?.showVersionNumber ?? true
+    const olivetinVersion = showVersionNumber
+      ? (window.initResponse?.currentVersion || t('diagnostics.unknown'))
+      : '[hidden]'
     const currentLanguage = locale.value || t('diagnostics.unknown')
     const currentLanguage = locale.value || t('diagnostics.unknown')
 
 
     let output = '';
     let output = '';
@@ -300,4 +303,4 @@ onMounted(() => {
   flex-direction: column;
   flex-direction: column;
   gap: 1em;
   gap: 1em;
 }
 }
-</style>
+</style>

+ 13 - 0
integration-tests/tests/xtermLinkHandling/config.yaml

@@ -0,0 +1,13 @@
+#
+# Integration Test Config: xterm link handling
+#
+
+listenAddressSingleHTTPFrontend: 0.0.0.0:1337
+
+logLevel: "DEBUG"
+checkForUpdates: false
+
+actions:
+  - title: Echo URL
+    shell: echo "See https://example.com for more info"
+    popupOnStart: execution-dialog-stdout-only

+ 69 - 0
integration-tests/tests/xtermLinkHandling/xtermLinkHandling.mjs

@@ -0,0 +1,69 @@
+import { describe, it, before, after } from 'mocha'
+import { expect } from 'chai'
+import { By, Condition } from 'selenium-webdriver'
+import {
+  getRootAndWait,
+  takeScreenshotOnFailure,
+  getTerminalBuffer,
+} from '../../lib/elements.js'
+
+describe('config: xtermLinkHandling', function () {
+  before(async function () {
+    await runner.start('xtermLinkHandling')
+  })
+
+  after(async () => {
+    await runner.stop()
+  })
+
+  afterEach(function () {
+    takeScreenshotOnFailure(this.currentTest, webdriver)
+  })
+
+  it('xterm output shows URL and link handling is configured', async function () {
+    await getRootAndWait()
+
+    await webdriver.wait(new Condition('wait for Echo URL button', async () => {
+      const btns = await webdriver.findElements(By.css('[title="Echo URL"]'))
+      return btns.length === 1
+    }), 10000)
+
+    const echoUrlButton = await webdriver.findElement(By.css('[title="Echo URL"]'))
+    await echoUrlButton.click()
+
+    await webdriver.wait(new Condition('wait for execution view', async () => {
+      const url = await webdriver.getCurrentUrl()
+      return url.includes('/logs/') && !url.endsWith('/logs')
+    }), 10000)
+
+    await webdriver.wait(new Condition('wait for execution status', async () => {
+      const statusElements = await webdriver.findElements(By.id('execution-dialog-status'))
+      return statusElements.length > 0
+    }), 5000)
+
+    await webdriver.wait(new Condition('wait for execution to finish', async () => {
+      try {
+        const statusElement = await webdriver.findElement(By.id('execution-dialog-status'))
+        const statusText = await statusElement.getText()
+        return !statusText.includes('Executing')
+      } catch (e) {
+        return false
+      }
+    }), 5000)
+
+    await webdriver.sleep(500)
+
+    const bufferText = await getTerminalBuffer()
+    expect(bufferText).to.not.be.null
+    expect(bufferText).to.include('https://example.com')
+
+    const linkHandlerSet = await webdriver.executeScript(`
+      try {
+        return !!(window.terminal && window.terminal.linkHandlerConfigured === true)
+      } catch (e) {
+        return false
+      }
+    `)
+    expect(linkHandlerSet).to.equal(true)
+  })
+})

+ 1 - 0
proto/olivetin/api/v1/olivetin.proto

@@ -51,6 +51,7 @@ message GetDashboardResponse {
 message EffectivePolicy {
 message EffectivePolicy {
 	bool show_diagnostics = 1;
 	bool show_diagnostics = 1;
 	bool show_log_list = 2;
 	bool show_log_list = 2;
+	bool show_version_number = 3;
 }
 }
 
 
 message GetDashboardRequest {
 message GetDashboardRequest {

+ 16 - 7
service/gen/olivetin/api/v1/olivetin.pb.go

@@ -410,11 +410,12 @@ func (x *GetDashboardResponse) GetDashboard() *Dashboard {
 }
 }
 
 
 type EffectivePolicy struct {
 type EffectivePolicy struct {
-	state           protoimpl.MessageState `protogen:"open.v1"`
-	ShowDiagnostics bool                   `protobuf:"varint,1,opt,name=show_diagnostics,json=showDiagnostics,proto3" json:"show_diagnostics,omitempty"`
-	ShowLogList     bool                   `protobuf:"varint,2,opt,name=show_log_list,json=showLogList,proto3" json:"show_log_list,omitempty"`
-	unknownFields   protoimpl.UnknownFields
-	sizeCache       protoimpl.SizeCache
+	state             protoimpl.MessageState `protogen:"open.v1"`
+	ShowDiagnostics   bool                   `protobuf:"varint,1,opt,name=show_diagnostics,json=showDiagnostics,proto3" json:"show_diagnostics,omitempty"`
+	ShowLogList       bool                   `protobuf:"varint,2,opt,name=show_log_list,json=showLogList,proto3" json:"show_log_list,omitempty"`
+	ShowVersionNumber bool                   `protobuf:"varint,3,opt,name=show_version_number,json=showVersionNumber,proto3" json:"show_version_number,omitempty"`
+	unknownFields     protoimpl.UnknownFields
+	sizeCache         protoimpl.SizeCache
 }
 }
 
 
 func (x *EffectivePolicy) Reset() {
 func (x *EffectivePolicy) Reset() {
@@ -461,6 +462,13 @@ func (x *EffectivePolicy) GetShowLogList() bool {
 	return false
 	return false
 }
 }
 
 
+func (x *EffectivePolicy) GetShowVersionNumber() bool {
+	if x != nil {
+		return x.ShowVersionNumber
+	}
+	return false
+}
+
 type GetDashboardRequest struct {
 type GetDashboardRequest struct {
 	state         protoimpl.MessageState `protogen:"open.v1"`
 	state         protoimpl.MessageState `protogen:"open.v1"`
 	Title         string                 `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
 	Title         string                 `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
@@ -3934,10 +3942,11 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
 	"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"f\n" +
 	"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"f\n" +
 	"\x14GetDashboardResponse\x12\x14\n" +
 	"\x14GetDashboardResponse\x12\x14\n" +
 	"\x05title\x18\x01 \x01(\tR\x05title\x128\n" +
 	"\x05title\x18\x01 \x01(\tR\x05title\x128\n" +
-	"\tdashboard\x18\x04 \x01(\v2\x1a.olivetin.api.v1.DashboardR\tdashboard\"`\n" +
+	"\tdashboard\x18\x04 \x01(\v2\x1a.olivetin.api.v1.DashboardR\tdashboard\"\x90\x01\n" +
 	"\x0fEffectivePolicy\x12)\n" +
 	"\x0fEffectivePolicy\x12)\n" +
 	"\x10show_diagnostics\x18\x01 \x01(\bR\x0fshowDiagnostics\x12\"\n" +
 	"\x10show_diagnostics\x18\x01 \x01(\bR\x0fshowDiagnostics\x12\"\n" +
-	"\rshow_log_list\x18\x02 \x01(\bR\vshowLogList\"k\n" +
+	"\rshow_log_list\x18\x02 \x01(\bR\vshowLogList\x12.\n" +
+	"\x13show_version_number\x18\x03 \x01(\bR\x11showVersionNumber\"k\n" +
 	"\x13GetDashboardRequest\x12\x14\n" +
 	"\x13GetDashboardRequest\x12\x14\n" +
 	"\x05title\x18\x01 \x01(\tR\x05title\x12\x1f\n" +
 	"\x05title\x18\x01 \x01(\tR\x05title\x12\x1f\n" +
 	"\ventity_type\x18\x02 \x01(\tR\n" +
 	"\ventity_type\x18\x02 \x01(\tR\n" +

+ 8 - 8
service/go.mod

@@ -1,6 +1,6 @@
 module github.com/OliveTin/OliveTin
 module github.com/OliveTin/OliveTin
 
 
-go 1.25.0
+go 1.25.6
 
 
 exclude google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884
 exclude google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884
 
 
@@ -10,7 +10,7 @@ require (
 	github.com/MicahParks/keyfunc/v3 v3.8.0
 	github.com/MicahParks/keyfunc/v3 v3.8.0
 	github.com/PaesslerAG/jsonpath v0.1.1
 	github.com/PaesslerAG/jsonpath v0.1.1
 	github.com/alexedwards/argon2id v1.0.0
 	github.com/alexedwards/argon2id v1.0.0
-	github.com/bufbuild/buf v1.65.0
+	github.com/bufbuild/buf v1.66.0
 	github.com/fsnotify/fsnotify v1.9.0
 	github.com/fsnotify/fsnotify v1.9.0
 	github.com/fzipp/gocyclo v0.6.0
 	github.com/fzipp/gocyclo v0.6.0
 	github.com/go-critic/go-critic v0.14.3
 	github.com/go-critic/go-critic v0.14.3
@@ -27,7 +27,7 @@ require (
 	github.com/sirupsen/logrus v1.9.4
 	github.com/sirupsen/logrus v1.9.4
 	github.com/stretchr/testify v1.11.1
 	github.com/stretchr/testify v1.11.1
 	go.akshayshah.org/connectproto v0.6.0
 	go.akshayshah.org/connectproto v0.6.0
-	golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a
+	golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa
 	golang.org/x/oauth2 v0.35.0
 	golang.org/x/oauth2 v0.35.0
 	golang.org/x/sys v0.41.0
 	golang.org/x/sys v0.41.0
 	google.golang.org/protobuf v1.36.11
 	google.golang.org/protobuf v1.36.11
@@ -45,7 +45,7 @@ require (
 	buf.build/go/bufplugin v0.9.0 // indirect
 	buf.build/go/bufplugin v0.9.0 // indirect
 	buf.build/go/bufprivateusage v0.1.0 // indirect
 	buf.build/go/bufprivateusage v0.1.0 // indirect
 	buf.build/go/interrupt v1.1.0 // indirect
 	buf.build/go/interrupt v1.1.0 // indirect
-	buf.build/go/protovalidate v1.1.2 // indirect
+	buf.build/go/protovalidate v1.1.3 // indirect
 	buf.build/go/protoyaml v0.6.0 // indirect
 	buf.build/go/protoyaml v0.6.0 // indirect
 	buf.build/go/spdx v0.2.0 // indirect
 	buf.build/go/spdx v0.2.0 // indirect
 	buf.build/go/standard v0.1.0 // indirect
 	buf.build/go/standard v0.1.0 // indirect
@@ -57,7 +57,7 @@ require (
 	github.com/PaesslerAG/gval v1.2.4 // indirect
 	github.com/PaesslerAG/gval v1.2.4 // indirect
 	github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
 	github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
-	github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e // indirect
+	github.com/bufbuild/protocompile v0.14.2-0.20260202185951-d02d3732d113 // indirect
 	github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 // indirect
 	github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/cli/browser v1.3.0 // indirect
 	github.com/cli/browser v1.3.0 // indirect
@@ -90,7 +90,7 @@ require (
 	github.com/gofrs/flock v0.13.0 // indirect
 	github.com/gofrs/flock v0.13.0 // indirect
 	github.com/google/cel-go v0.27.0 // indirect
 	github.com/google/cel-go v0.27.0 // indirect
 	github.com/google/go-cmp v0.7.0 // indirect
 	github.com/google/go-cmp v0.7.0 // indirect
-	github.com/google/go-containerregistry v0.20.7 // indirect
+	github.com/google/go-containerregistry v0.21.0 // indirect
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jdx/go-netrc v1.0.0 // indirect
 	github.com/jdx/go-netrc v1.0.0 // indirect
@@ -155,8 +155,8 @@ require (
 	golang.org/x/text v0.34.0 // indirect
 	golang.org/x/text v0.34.0 // indirect
 	golang.org/x/time v0.14.0 // indirect
 	golang.org/x/time v0.14.0 // indirect
 	golang.org/x/tools v0.42.0 // indirect
 	golang.org/x/tools v0.42.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
 	google.golang.org/grpc v1.75.1 // indirect
 	google.golang.org/grpc v1.75.1 // indirect
 	mvdan.cc/xurls/v2 v2.6.0 // indirect
 	mvdan.cc/xurls/v2 v2.6.0 // indirect
 	pluginrpc.com/pluginrpc v0.5.0 // indirect
 	pluginrpc.com/pluginrpc v0.5.0 // indirect

+ 15 - 0
service/go.sum

@@ -32,6 +32,8 @@ buf.build/go/protovalidate v1.1.0 h1:pQqEQRpOo4SqS60qkvmhLTTQU9JwzEvdyiqAtXa5SeY
 buf.build/go/protovalidate v1.1.0/go.mod h1:bGZcPiAQDC3ErCHK3t74jSoJDFOs2JH3d7LWuTEIdss=
 buf.build/go/protovalidate v1.1.0/go.mod h1:bGZcPiAQDC3ErCHK3t74jSoJDFOs2JH3d7LWuTEIdss=
 buf.build/go/protovalidate v1.1.2 h1:83vYHoY8f34hB8MeitGaYE3CGVPFxwdEUuskh5qQpA0=
 buf.build/go/protovalidate v1.1.2 h1:83vYHoY8f34hB8MeitGaYE3CGVPFxwdEUuskh5qQpA0=
 buf.build/go/protovalidate v1.1.2/go.mod h1:Ez3z+w4c+wG+EpW8ovgZaZPnPl2XVF6kaxgcv1NG/QE=
 buf.build/go/protovalidate v1.1.2/go.mod h1:Ez3z+w4c+wG+EpW8ovgZaZPnPl2XVF6kaxgcv1NG/QE=
+buf.build/go/protovalidate v1.1.3 h1:m2GVEgQWd7rk+vIoAZ+f0ygGjvQTuqPQapBBdcpWVPE=
+buf.build/go/protovalidate v1.1.3/go.mod h1:9XIuohWz+kj+9JVn3WQneHA5LZP50mjvneZMnbLkiIE=
 buf.build/go/protoyaml v0.6.0 h1:Nzz1lvcXF8YgNZXk+voPPwdU8FjDPTUV4ndNTXN0n2w=
 buf.build/go/protoyaml v0.6.0 h1:Nzz1lvcXF8YgNZXk+voPPwdU8FjDPTUV4ndNTXN0n2w=
 buf.build/go/protoyaml v0.6.0/go.mod h1:RgUOsBu/GYKLDSIRgQXniXbNgFlGEZnQpRAUdLAFV2Q=
 buf.build/go/protoyaml v0.6.0/go.mod h1:RgUOsBu/GYKLDSIRgQXniXbNgFlGEZnQpRAUdLAFV2Q=
 buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw=
 buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw=
@@ -84,6 +86,8 @@ github.com/bufbuild/buf v1.64.0 h1:puHWFcVKmZFSu4KuaN0kZiQ32n7VVc3un1FeLU77XUs=
 github.com/bufbuild/buf v1.64.0/go.mod h1:U4ISwkjZXRLMaCkPG9zp1xY3xHEIwhCFwyNAaA56SGw=
 github.com/bufbuild/buf v1.64.0/go.mod h1:U4ISwkjZXRLMaCkPG9zp1xY3xHEIwhCFwyNAaA56SGw=
 github.com/bufbuild/buf v1.65.0 h1:f2BzeCY9rRh9P5KD340ZoPAaFLTkssoUTHx7lpqozgg=
 github.com/bufbuild/buf v1.65.0 h1:f2BzeCY9rRh9P5KD340ZoPAaFLTkssoUTHx7lpqozgg=
 github.com/bufbuild/buf v1.65.0/go.mod h1:7SAs2YqGpPXHqBBXBeYQbCzY0OQq4Jbg6XCqirEiYvQ=
 github.com/bufbuild/buf v1.65.0/go.mod h1:7SAs2YqGpPXHqBBXBeYQbCzY0OQq4Jbg6XCqirEiYvQ=
+github.com/bufbuild/buf v1.66.0 h1:6kksYJpu6r45bvPJSTwNSwRqiAjrwB9YyU7skjNzFVo=
+github.com/bufbuild/buf v1.66.0/go.mod h1:tWVlwtIPZ7kzlCB9D0hbbfrroT0GNCybPdPQXq1i1Ac=
 github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e h1:LQA+1MyiPkolGHJGC2GMDC5Xu+0RDVH6jGMKech7Exs=
 github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e h1:LQA+1MyiPkolGHJGC2GMDC5Xu+0RDVH6jGMKech7Exs=
 github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI=
 github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI=
 github.com/bufbuild/protocompile v0.14.2-0.20260105175043-4d8d90b1c6b8 h1:cQYwUyAzyMmYr7AyJU1C6pVCpUrJJBkmx7UunZosxxs=
 github.com/bufbuild/protocompile v0.14.2-0.20260105175043-4d8d90b1c6b8 h1:cQYwUyAzyMmYr7AyJU1C6pVCpUrJJBkmx7UunZosxxs=
@@ -92,6 +96,8 @@ github.com/bufbuild/protocompile v0.14.2-0.20260120135352-a3ed5cd7a608 h1:3aRREB
 github.com/bufbuild/protocompile v0.14.2-0.20260120135352-a3ed5cd7a608/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI=
 github.com/bufbuild/protocompile v0.14.2-0.20260120135352-a3ed5cd7a608/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI=
 github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e h1:emH16Bf1w4C0cJ3ge4QtBAl4sIYJe23EfpWH0SpA9co=
 github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e h1:emH16Bf1w4C0cJ3ge4QtBAl4sIYJe23EfpWH0SpA9co=
 github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e/go.mod h1:cxhE8h+14t0Yxq2H9MV/UggzQ1L0gh0t2tJobITWsBE=
 github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e/go.mod h1:cxhE8h+14t0Yxq2H9MV/UggzQ1L0gh0t2tJobITWsBE=
+github.com/bufbuild/protocompile v0.14.2-0.20260202185951-d02d3732d113 h1:nxt1QhP9rMQNFhHTdcNFwJ9wKCSdBjd28gz+qGDv4kM=
+github.com/bufbuild/protocompile v0.14.2-0.20260202185951-d02d3732d113/go.mod h1:cxhE8h+14t0Yxq2H9MV/UggzQ1L0gh0t2tJobITWsBE=
 github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 h1:V1xulAoqLqVg44rY97xOR+mQpD2N+GzhMHVwJ030WEU=
 github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 h1:V1xulAoqLqVg44rY97xOR+mQpD2N+GzhMHVwJ030WEU=
 github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ=
 github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ=
 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
@@ -200,6 +206,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I=
 github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I=
 github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM=
 github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM=
+github.com/google/go-containerregistry v0.21.0 h1:ocqxUOczFwAZQBMNE7kuzfqvDe0VWoZxQMOesXreCDI=
+github.com/google/go-containerregistry v0.21.0/go.mod h1:ctO5aCaewH4AK1AumSF5DPW+0+R+d2FmylMJdp5G7p0=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
 github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
@@ -375,6 +383,7 @@ go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIl
 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
 go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
 go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
@@ -421,6 +430,8 @@ golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7
 golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
 golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
 golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
 golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
 golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
 golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
+golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
+golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
 golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
 golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
 golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
 golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
 golang.org/x/exp/typeparams v0.0.0-20251219203646-944ab1f22d93 h1:PbC785RGO6yPO051ItgbG/adwoKRWC0VS7kXXeD/iqk=
 golang.org/x/exp/typeparams v0.0.0-20251219203646-944ab1f22d93 h1:PbC785RGO6yPO051ItgbG/adwoKRWC0VS7kXXeD/iqk=
@@ -519,6 +530,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d h1:
 google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw=
 google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw=
 google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
 google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
 google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
 google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
+google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d h1:EocjzKLywydp5uZ5tJ79iP6Q0UjDnyiHkGRWxuPBP8s=
+google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
@@ -527,6 +540,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d h1:
 google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
 google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
 google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
 google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
 google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
 google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
 google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

+ 13 - 4
service/internal/api/api.go

@@ -710,7 +710,9 @@ func (api *oliveTinAPI) WhoAmI(ctx ctx.Context, req *connect.Request[apiv1.WhoAm
 }
 }
 
 
 func (api *oliveTinAPI) SosReport(ctx ctx.Context, req *connect.Request[apiv1.SosReportRequest]) (*connect.Response[apiv1.SosReportResponse], error) {
 func (api *oliveTinAPI) SosReport(ctx ctx.Context, req *connect.Request[apiv1.SosReportRequest]) (*connect.Response[apiv1.SosReportResponse], error) {
-	sos := installationinfo.GetSosReport()
+	user := auth.UserFromApiCall(ctx, req, api.cfg)
+	redactVersion := !user.EffectivePolicy.ShowVersionNumber
+	sos := installationinfo.GetSosReport(redactVersion)
 
 
 	if !api.cfg.InsecureAllowDumpSos {
 	if !api.cfg.InsecureAllowDumpSos {
 		log.Info(sos)
 		log.Info(sos)
@@ -914,12 +916,19 @@ func (api *oliveTinAPI) Init(ctx ctx.Context, req *connect.Request[apiv1.InitReq
 
 
 	loginRequired := user.IsGuest() && api.cfg.AuthRequireGuestsToLogin
 	loginRequired := user.IsGuest() && api.cfg.AuthRequireGuestsToLogin
 
 
+	showVersion := user.EffectivePolicy.ShowVersionNumber
+	currentVersion := ""
+	availableVersion := ""
+	if showVersion {
+		currentVersion = installationinfo.Build.Version
+		availableVersion = installationinfo.Runtime.AvailableVersion
+	}
 	res := &apiv1.InitResponse{
 	res := &apiv1.InitResponse{
 		ShowFooter:                api.cfg.ShowFooter,
 		ShowFooter:                api.cfg.ShowFooter,
 		ShowNavigation:            api.cfg.ShowNavigation,
 		ShowNavigation:            api.cfg.ShowNavigation,
-		ShowNewVersions:           api.cfg.ShowNewVersions,
-		AvailableVersion:          installationinfo.Runtime.AvailableVersion,
-		CurrentVersion:            installationinfo.Build.Version,
+		ShowNewVersions:           showVersion && api.cfg.ShowNewVersions,
+		AvailableVersion:          availableVersion,
+		CurrentVersion:            currentVersion,
 		PageTitle:                 api.cfg.PageTitle,
 		PageTitle:                 api.cfg.PageTitle,
 		SectionNavigationStyle:    api.cfg.SectionNavigationStyle,
 		SectionNavigationStyle:    api.cfg.SectionNavigationStyle,
 		DefaultIconForBack:        api.cfg.DefaultIconForBack,
 		DefaultIconForBack:        api.cfg.DefaultIconForBack,

+ 3 - 2
service/internal/api/apiActions.go

@@ -55,8 +55,9 @@ func matchesEntity(binding *executor.ActionBinding, entity *entities.Entity) boo
 
 
 func buildEffectivePolicy(policy *config.ConfigurationPolicy) *apiv1.EffectivePolicy {
 func buildEffectivePolicy(policy *config.ConfigurationPolicy) *apiv1.EffectivePolicy {
 	ret := &apiv1.EffectivePolicy{
 	ret := &apiv1.EffectivePolicy{
-		ShowDiagnostics: policy.ShowDiagnostics,
-		ShowLogList:     policy.ShowLogList,
+		ShowDiagnostics:   policy.ShowDiagnostics,
+		ShowLogList:       policy.ShowLogList,
+		ShowVersionNumber: policy.ShowVersionNumber,
 	}
 	}
 
 
 	return ret
 	return ret

+ 7 - 2
service/internal/auth/authpublic/authenticateduser.go

@@ -76,8 +76,9 @@ func (u *AuthenticatedUser) BuildUserAcls(cfg *config.Config) {
 
 
 func getEffectivePolicy(cfg *config.Config, u *AuthenticatedUser) *config.ConfigurationPolicy {
 func getEffectivePolicy(cfg *config.Config, u *AuthenticatedUser) *config.ConfigurationPolicy {
 	ret := &config.ConfigurationPolicy{
 	ret := &config.ConfigurationPolicy{
-		ShowDiagnostics: cfg.DefaultPolicy.ShowDiagnostics,
-		ShowLogList:     cfg.DefaultPolicy.ShowLogList,
+		ShowDiagnostics:   cfg.DefaultPolicy.ShowDiagnostics,
+		ShowLogList:       cfg.DefaultPolicy.ShowLogList,
+		ShowVersionNumber: cfg.DefaultPolicy.ShowVersionNumber,
 	}
 	}
 
 
 	for _, acl := range cfg.AccessControlLists {
 	for _, acl := range cfg.AccessControlLists {
@@ -98,5 +99,9 @@ func buildConfigurationPolicy(ret *config.ConfigurationPolicy, policy config.Con
 		ret.ShowLogList = policy.ShowLogList
 		ret.ShowLogList = policy.ShowLogList
 	}
 	}
 
 
+	if policy.ShowVersionNumber {
+		ret.ShowVersionNumber = policy.ShowVersionNumber
+	}
+
 	return ret
 	return ret
 }
 }

+ 27 - 17
service/internal/auth/otoauth2/restapi_auth_oauth2.go

@@ -11,6 +11,7 @@ import (
 	"io"
 	"io"
 	"net/http"
 	"net/http"
 	"os"
 	"os"
+	"sync"
 	"time"
 	"time"
 
 
 	authTypes "github.com/OliveTin/OliveTin/internal/auth/authpublic"
 	authTypes "github.com/OliveTin/OliveTin/internal/auth/authpublic"
@@ -21,6 +22,7 @@ import (
 
 
 type OAuth2Handler struct {
 type OAuth2Handler struct {
 	cfg                 *config.Config
 	cfg                 *config.Config
+	mu                  sync.RWMutex
 	registeredStates    map[string]*oauth2State
 	registeredStates    map[string]*oauth2State
 	registeredProviders map[string]*oauth2.Config
 	registeredProviders map[string]*oauth2.Config
 }
 }
@@ -144,11 +146,13 @@ func (h *OAuth2Handler) HandleOAuthLogin(w http.ResponseWriter, r *http.Request)
 		return
 		return
 	}
 	}
 
 
+	h.mu.Lock()
 	h.registeredStates[state] = &oauth2State{
 	h.registeredStates[state] = &oauth2State{
 		providerConfig: provider,
 		providerConfig: provider,
 		providerName:   providerName,
 		providerName:   providerName,
 		Username:       "",
 		Username:       "",
 	}
 	}
+	h.mu.Unlock()
 
 
 	h.setOAuthCallbackCookie(w, r, "olivetin-sid-oauth", state)
 	h.setOAuthCallbackCookie(w, r, "olivetin-sid-oauth", state)
 
 
@@ -177,7 +181,9 @@ func (h *OAuth2Handler) checkOAuthCallbackCookie(w http.ResponseWriter, r *http.
 		return nil, state, false
 		return nil, state, false
 	}
 	}
 
 
+	h.mu.RLock()
 	registeredState, ok := h.registeredStates[state]
 	registeredState, ok := h.registeredStates[state]
+	h.mu.RUnlock()
 	if !ok {
 	if !ok {
 		log.Errorf("State not found in server: %v", state)
 		log.Errorf("State not found in server: %v", state)
 		http.Error(w, "State not found in server", http.StatusBadRequest)
 		http.Error(w, "State not found in server", http.StatusBadRequest)
@@ -287,8 +293,10 @@ func (h *OAuth2Handler) HandleOAuthCallback(w http.ResponseWriter, r *http.Reque
 	userInfoClient := h.createUserInfoClient(ctx, registeredState.providerConfig, tok, clientSettings)
 	userInfoClient := h.createUserInfoClient(ctx, registeredState.providerConfig, tok, clientSettings)
 	userinfo := getUserInfo(h.cfg, userInfoClient, providerConfig)
 	userinfo := getUserInfo(h.cfg, userInfoClient, providerConfig)
 
 
+	h.mu.Lock()
 	h.registeredStates[state].Username = userinfo.Username
 	h.registeredStates[state].Username = userinfo.Username
 	h.registeredStates[state].Usergroup = h.computeUsergroup(userinfo, providerConfig)
 	h.registeredStates[state].Usergroup = h.computeUsergroup(userinfo, providerConfig)
+	h.mu.Unlock()
 
 
 	http.Redirect(w, r, "/", http.StatusFound)
 	http.Redirect(w, r, "/", http.StatusFound)
 }
 }
@@ -366,34 +374,36 @@ func getDataField(data map[string]any, field string) string {
 	return stringVal
 	return stringVal
 }
 }
 
 
-func (h *OAuth2Handler) CheckUserFromOAuth2Cookie(context *authTypes.AuthCheckingContext) *authTypes.AuthenticatedUser {
-	cookie, err := context.Request.Cookie("olivetin-sid-oauth")
-
-	user := &authTypes.AuthenticatedUser{}
-
-	if err != nil {
-		return nil
+func (h *OAuth2Handler) lookupOAuth2UserByState(state string) (*authTypes.AuthenticatedUser, bool) {
+	h.mu.RLock()
+	serverState, found := h.registeredStates[state]
+	if !found {
+		h.mu.RUnlock()
+		return nil, false
+	}
+	user := &authTypes.AuthenticatedUser{
+		Username:      serverState.Username,
+		UsergroupLine: serverState.Usergroup,
+		Provider:      "oauth2",
+		SID:           state,
 	}
 	}
+	h.mu.RUnlock()
+	return user, true
+}
 
 
-	if cookie.Value == "" {
+func (h *OAuth2Handler) CheckUserFromOAuth2Cookie(context *authTypes.AuthCheckingContext) *authTypes.AuthenticatedUser {
+	cookie, err := context.Request.Cookie("olivetin-sid-oauth")
+	if err != nil || cookie.Value == "" {
 		return nil
 		return nil
 	}
 	}
 
 
-	serverState, found := h.registeredStates[cookie.Value]
-
+	user, found := h.lookupOAuth2UserByState(cookie.Value)
 	if !found {
 	if !found {
 		log.WithFields(log.Fields{
 		log.WithFields(log.Fields{
 			"sid":      cookie.Value,
 			"sid":      cookie.Value,
 			"provider": "oauth2",
 			"provider": "oauth2",
 		}).Warnf("Stale session")
 		}).Warnf("Stale session")
-
 		return nil
 		return nil
 	}
 	}
-
-	user.Username = serverState.Username
-	user.UsergroupLine = serverState.Usergroup
-	user.Provider = "oauth2"
-	user.SID = cookie.Value
-
 	return user
 	return user
 }
 }

+ 4 - 2
service/internal/config/config.go

@@ -98,8 +98,9 @@ type AccessControlList struct {
 
 
 // ConfigurationPolicy defines global settings which are overridden with an ACL.
 // ConfigurationPolicy defines global settings which are overridden with an ACL.
 type ConfigurationPolicy struct {
 type ConfigurationPolicy struct {
-	ShowDiagnostics bool `koanf:"showDiagnostics"`
-	ShowLogList     bool `koanf:"showLogList"`
+	ShowDiagnostics   bool `koanf:"showDiagnostics"`
+	ShowLogList       bool `koanf:"showLogList"`
+	ShowVersionNumber bool `koanf:"showVersionNumber"`
 }
 }
 
 
 type PrometheusConfig struct {
 type PrometheusConfig struct {
@@ -297,6 +298,7 @@ func DefaultConfigWithBasePort(basePort int) *Config {
 
 
 	config.DefaultPolicy.ShowDiagnostics = true
 	config.DefaultPolicy.ShowDiagnostics = true
 	config.DefaultPolicy.ShowLogList = true
 	config.DefaultPolicy.ShowLogList = true
+	config.DefaultPolicy.ShowVersionNumber = true
 
 
 	return &config
 	return &config
 }
 }

+ 1 - 0
service/internal/config/sanitize.go

@@ -164,6 +164,7 @@ func (cfg *Config) sanitizeAuthRequireGuestsToLogin() {
 		cfg.DefaultPermissions.View = false
 		cfg.DefaultPermissions.View = false
 		cfg.DefaultPermissions.Exec = false
 		cfg.DefaultPermissions.Exec = false
 		cfg.DefaultPermissions.Logs = false
 		cfg.DefaultPermissions.Logs = false
+		cfg.DefaultPermissions.Kill = false
 	}
 	}
 }
 }
 
 

+ 11 - 3
service/internal/installationinfo/sosreport.go

@@ -40,15 +40,23 @@ func configToSosreport(cfg *config.Config) *sosReportConfig {
 	}
 	}
 }
 }
 
 
-func GetSosReport() string {
+func GetSosReport(redactVersion bool) string {
 	ret := ""
 	ret := ""
 
 
 	ret += "### SOSREPORT START (copy all text to SOSREPORT END)\n"
 	ret += "### SOSREPORT START (copy all text to SOSREPORT END)\n"
 
 
-	out, _ := yaml.Marshal(Build)
+	buildForReport := *Build
+	if redactVersion {
+		buildForReport.Version = "[redacted]"
+	}
+	out, _ := yaml.Marshal(&buildForReport)
 	ret += fmt.Sprintf("# Build: \n%+v\n", string(out))
 	ret += fmt.Sprintf("# Build: \n%+v\n", string(out))
 
 
-	out, _ = yaml.Marshal(Runtime)
+	runtimeForReport := *Runtime
+	if redactVersion {
+		runtimeForReport.AvailableVersion = "[redacted]"
+	}
+	out, _ = yaml.Marshal(&runtimeForReport)
 	ret += fmt.Sprintf("# Runtime:\n%+v\n", string(out))
 	ret += fmt.Sprintf("# Runtime:\n%+v\n", string(out))
 
 
 	out, _ = yaml.Marshal(configToSosreport(Config))
 	out, _ = yaml.Marshal(configToSosreport(Config))

Деякі файли не було показано, через те що забагато файлів було змінено