Tentacle support enhancements.

This commit is contained in:
Storm Dragon committed 2026-11-04 12:10:41 -05:00
1 parent 4cf3335017
commit 24261edb3d
12 files changed
+560 -30

No files matched your search

+40 -1
View File
@@ -43,6 +43,22 @@ func (runtime *tentacleRuntime) interact(ctx context.Context, command tentacleCo
defer stopCommand()
stop := context.AfterFunc(command.session.OutputContext(), cancel)
defer stop()
// Policy cancellation can interrupt setup before a lease exists, when no
// remote.revoked event is emitted. Tell the original continuation to stop its
// inspection/setup workers before releasing the interaction slot. Never pass
// cancelled form values or reuse the cancelled interaction context.
cleanupNext := ""
var approvedEpoch uint64
defer func() {
if approvedEpoch != 0 {
command.session.RevokeRemoteEpoch(approvedEpoch)
}
if len(command.session.RemoteGrants()) != 0 && cleanupNext != "" && command.session.OutputContext().Err() == nil {
cleanupCtx, stopCleanup := context.WithTimeout(context.Background(), 2*time.Second)
defer stopCleanup()
runtime.callInteraction(cleanupCtx, command, cleanupNext, tentaclewire.Submission{Cancelled: true})
}
}()
var temporaryFiles []string
defer func() {
for _, path := range temporaryFiles {
@@ -60,6 +76,26 @@ func (runtime *tentacleRuntime) interact(ctx context.Context, command tentacleCo
if runtime.output.gate.Locked() || runtime.output.gate.Suspended() {
return
}
cleanupNext = value.Next
if value.Remote != nil {
action, approvalErr := runtime.authorize_remote_session(ctx, command.session, value.Remote.Role, value.Remote.Grants)
submission := tentaclewire.Submission{}
if approvalErr != nil {
submission.Cancelled = true
submission.Error = "Remote access was not approved."
} else {
approvedEpoch = action.Epoch
submission.Remote = &action
}
raw, err = runtime.callInteraction(ctx, command, value.Next, submission)
if err == nil && submission.Cancelled {
cleanupNext = ""
}
if err != nil || approvalErr != nil {
return
}
continue
}
if value.Clipboard != nil {
copyCtx, stopCopy := context.WithTimeout(ctx, time.Second)
err = writeClipboardText(copyCtx, *value.Clipboard)
@@ -98,6 +134,7 @@ func (runtime *tentacleRuntime) interact(ctx context.Context, command tentacleCo
if value.Form != nil {
if runtime.forms == nil {
runtime.callInteraction(ctx, command, value.Next, tentaclewire.Submission{Cancelled: true})
cleanupNext = ""
runtime.report("Internal UI unavailable.")
return
}
@@ -106,6 +143,7 @@ func (runtime *tentacleRuntime) interact(ctx context.Context, command tentacleCo
if openErr != nil {
closeForm()
runtime.callInteraction(ctx, command, value.Next, tentaclewire.Submission{Cancelled: true})
cleanupNext = ""
runtime.report("Internal UI unavailable.")
return
}
@@ -158,6 +196,7 @@ func (runtime *tentacleRuntime) interact(ctx context.Context, command tentacleCo
return
}
if submission.Cancelled {
cleanupNext = ""
return
}
}
@@ -216,7 +255,7 @@ func (runtime *tentacleRuntime) waitTentacleForm(ctx context.Context, command te
return internalUIResult{Cancelled: true}, nil
}
status, err := tentaclewire.DecodeInteraction(raw)
if err != nil || status.Form != nil || status.Source != "" || status.Next != "" || status.Reader != nil {
if err != nil || status.Form != nil || status.Source != "" || status.Next != "" || status.Reader != nil || status.Remote != nil {
command.session.Disable()
return internalUIResult{Cancelled: true}, nil
}
@@ -0,0 +1,317 @@
// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"bufio"
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"encoding/json"
"io"
"log/slog"
"math/big"
"net"
"os"
"path/filepath"
"strings"
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/input"
"git.stormux.org/storm/cthulhu/internal/interaction"
"git.stormux.org/storm/cthulhu/internal/tentacle"
)
// This uses the source-built external executable and real host/SDK IPC. Only
// OpenLocal submissions are synthetic; it cannot prove physical-source input.
func remote_command_fixture(t *testing.T) (*tentacleRuntime, *consentTestForms, input.Command) {
t.Helper()
program := os.Getenv("CTHULHU_REMOTE_BINARY")
if program == "" {
t.Skip("requires the separately built Remote executable")
}
directory := t.TempDir()
spec := tentacle.Spec{ID: "remote", Program: []string{program, "--data-dir", filepath.Join(directory, "state")}, Presentation: true, Interaction: true, SessionEvents: true, RemoteGrants: []string{"output-observe", "remote-present"}}
data, _ := json.Marshal([]tentacle.Spec{spec})
launchPath := filepath.Join(directory, "launch.json")
if err := os.WriteFile(launchPath, data, 0600); err != nil {
t.Fatal(err)
}
bindings := &tentacleBindingRecorder{}
runtime := newTentacleRuntime(slog.New(slog.DiscardHandler), bindings, launchPath, "")
runtime.output = tentacleOutputFixture(t)
runtime.remotePolicy = tentacle.NewRemotePolicy()
runtime.output.gate.remotePolicy = runtime.remotePolicy
runtime.newManager = func() (*tentacle.Manager, error) { return tentacle.NewManager(tentacle.DefaultLimits(), nil) }
forms := &consentTestForms{requests: make(chan consentFormRequest, 1)}
runtime.forms = forms
ctx, cancel := context.WithCancel(t.Context())
t.Cleanup(func() {
cancel()
stopCtx, stopCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer stopCancel()
if err := runtime.Stop(stopCtx); err != nil {
t.Error(err)
}
})
if err := runtime.Start(ctx); err != nil {
t.Fatal(err)
}
var command input.Command
waitTentacle(t, func() bool {
runtime.mutex.Lock()
defer runtime.mutex.Unlock()
for id, registered := range runtime.commands {
if registered.id == "remote" {
command = id
return true
}
}
return false
})
runtime.Invoke(ctx, command)
return runtime, forms, command
}
func next_remote_form(t *testing.T, forms *consentTestForms) consentFormRequest {
t.Helper()
select {
case request := <-forms.requests:
if request.ctx.Err() != nil {
t.Fatal("form already cancelled")
}
if _, err := interaction.NewSession(request.form); err != nil {
t.Fatal("invalid form", err)
}
return request
case <-time.After(5 * time.Second):
t.Fatal("Remote did not present its next local form")
return consentFormRequest{}
}
}
func select_remote_relay(t *testing.T, forms *consentTestForms, address, role string) consentFormRequest {
t.Helper()
selection := next_remote_form(t, forms)
if !strings.Contains(selection.form.Title, "Remote relay and role") || selection.form.Elements[0].ID != "cancel" {
t.Fatal("missing safe endpoint/role selection")
}
// Required entry validation applies to every button in native forms. An
// empty relay must not prevent the initially focused Cancel from working.
selectionSession, _ := interaction.NewSession(selection.form)
if effect := selectionSession.Handle(interaction.Event{Key: interaction.Enter}); effect.Activated != "cancel" {
t.Fatal("empty relay entries blocked the Cancel button")
}
host, port, err := net.SplitHostPort(address)
if err != nil {
t.Fatal(err)
}
selection.reply <- internalUIResult{Activated: "review-access", Values: map[string]interaction.Value{"host": {Text: host}, "port": {Text: port}, "role": {Selected: role}}}
consent := next_remote_form(t, forms)
if !strings.Contains(consent.form.Title, "Remote access for remote") || consent.form.Elements[0].ID != "cancel" {
t.Fatal("connection skipped reader-owned consent")
}
for _, element := range consent.form.Elements {
if element.Checked {
t.Fatal("permission preselected")
}
}
return consent
}
func approve_remote_form(t *testing.T, forms *consentTestForms, consent consentFormRequest, role string) consentFormRequest {
t.Helper()
grant := "remote-present"
if role == "controlled" {
grant = "output-observe"
}
consent.reply <- internalUIResult{Activated: "approve", Values: map[string]interaction.Value{grant: {Checked: true}}}
credentials := next_remote_form(t, forms)
if credentials.form.Title != "Connect Remote relay" {
t.Fatal("approval did not reach connection controls", credentials.form.Title)
}
for _, element := range credentials.form.Elements {
if element.ID == "channel-key" && (!element.Secret || element.Text != "") {
t.Fatal("unmasked or prefilled channel key")
}
}
return credentials
}
func TestRemoteConnectionCommandConsentDenialAndPolicyCancellation(t *testing.T) {
for _, mode := range []string{"deny", "empty", "lock", "handoff"} {
t.Run(mode, func(t *testing.T) {
runtime, forms, command := remote_command_fixture(t)
consent := select_remote_relay(t, forms, "127.0.0.1:6837", "controller")
switch mode {
case "deny":
consent.reply <- internalUIResult{Cancelled: true}
case "empty":
consent.reply <- internalUIResult{Activated: "approve"}
case "lock", "handoff":
runtime.remotePolicy.SetAvailable(false)
runtime.remotePolicy.SetAvailable(true)
}
waitTentacle(t, func() bool { return len(runtime.resultSlots) == 0 })
select {
case form := <-forms.requests:
t.Fatal("denied consent opened connection controls", form.form.Title)
default:
}
// The command must recover after a denied/cancelled review, without
// keeping an awaiting flag or reviving the previous approval.
runtime.Invoke(t.Context(), command)
form := next_remote_form(t, forms)
if !strings.Contains(form.form.Title, "Remote relay and role") {
t.Fatal("stale setup survived cancellation")
}
form.reply <- internalUIResult{Cancelled: true}
waitTentacle(t, func() bool { return len(runtime.resultSlots) == 0 })
})
}
}
func TestRemoteConnectionCommandCancelsInspectionOnHandoff(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
accepted := make(chan struct{})
closed := make(chan struct{})
go func() {
connection, err := listener.Accept()
if err != nil {
return
}
defer connection.Close()
connection.SetDeadline(time.Now().Add(5 * time.Second))
close(accepted)
io.Copy(io.Discard, connection)
close(closed)
}()
runtime, forms, _ := remote_command_fixture(t)
consent := select_remote_relay(t, forms, listener.Addr().String(), "controller")
credentials := approve_remote_form(t, forms, consent, "controller")
credentials.reply <- internalUIResult{Activated: "inspect-certificate"}
inspection := next_remote_form(t, forms)
select {
case <-accepted:
case <-time.After(5 * time.Second):
t.Fatal("inspection did not open its socket")
}
runtime.remotePolicy.SetAvailable(false)
runtime.remotePolicy.SetAvailable(true)
select {
case <-inspection.ctx.Done():
case <-time.After(time.Second):
t.Fatal("handoff retained the inspection form")
}
select {
case <-closed:
case <-time.After(time.Second):
t.Fatal("handoff retained a pre-lease inspection socket")
}
waitTentacle(t, func() bool { return len(runtime.resultSlots) == 0 })
}
func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
for _, role := range []string{"controller", "controlled"} {
t.Run(role, func(t *testing.T) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
template := &x509.Certificate{SerialNumber: big.NewInt(1), NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour), KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}}
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
if err != nil {
t.Fatal(err)
}
listener, err := tls.Listen("tcp", "127.0.0.1:0", &tls.Config{Certificates: []tls.Certificate{{Certificate: [][]byte{der}, PrivateKey: key}}, MinVersion: tls.VersionTLS12})
if err != nil {
t.Fatal(err)
}
defer listener.Close()
probed, joined, closed := make(chan struct{}), make(chan struct{}), make(chan struct{})
go func() {
probe, err := listener.Accept()
if err != nil {
return
}
probe.SetDeadline(time.Now().Add(5 * time.Second))
data, _ := io.ReadAll(probe)
probe.Close()
if len(data) != 0 {
t.Error("certificate inspection sent application data")
}
close(probed)
connection, err := listener.Accept()
if err != nil {
return
}
defer connection.Close()
connection.SetDeadline(time.Now().Add(10 * time.Second))
reader := bufio.NewReader(connection)
if _, err := reader.ReadBytes('\n'); err != nil {
return
}
line, err := reader.ReadBytes('\n')
var join struct {
Type string `json:"type"`
Channel string `json:"channel"`
ConnectionType string `json:"connection_type"`
}
wantRole := "master"
if role == "controlled" {
wantRole = "slave"
}
if err != nil || json.Unmarshal(line, &join) != nil || join.Type != "join" || join.Channel != "synthetic-fixture" || join.ConnectionType != wantRole {
t.Error("invalid relay join or wrong selected role")
return
}
io.WriteString(connection, `{"type":"channel_joined","channel":"synthetic-fixture","clients":[]}`+"\n")
close(joined)
io.Copy(io.Discard, reader)
close(closed)
}()
runtime, forms, _ := remote_command_fixture(t)
consent := select_remote_relay(t, forms, listener.Addr().String(), role)
credentials := approve_remote_form(t, forms, consent, role)
credentials.reply <- internalUIResult{Activated: "inspect-certificate"}
inspection := next_remote_form(t, forms)
select {
case <-probed:
case <-time.After(5 * time.Second):
t.Fatal("certificate probe did not finish")
}
for inspection.form.Title != "Review Remote relay certificate" {
inspection.reply <- internalUIResult{Activated: "refresh"}
inspection = next_remote_form(t, forms)
}
inspection.reply <- internalUIResult{Activated: "trust-once"}
credentials = next_remote_form(t, forms)
credentials.reply <- internalUIResult{Activated: "connect", Values: map[string]interaction.Value{"channel-key": {Text: "synthetic-fixture"}}}
status := next_remote_form(t, forms)
select {
case <-joined:
case <-time.After(5 * time.Second):
t.Fatal("Remote did not join the private relay")
}
if status.form.Title != "Remote relay status" {
t.Fatal("missing connection status")
}
status.reply <- internalUIResult{Activated: "cancel"}
select {
case <-closed:
case <-time.After(5 * time.Second):
t.Fatal("Disconnect and close retained the relay")
}
waitTentacle(t, func() bool { return len(runtime.resultSlots) == 0 })
})
}
}
+29 -16
View File
@@ -17,19 +17,12 @@ type localTentacleForms interface {
OpenLocal(context.Context, interaction.Form) (<-chan internalUIResult, error)
}
// authorize_remote is a core entry point, not a registered Tentacle command or
// child RPC. A future connection command must resolve the installed session and
// role before calling it. A synthetic shortcut may request a dialog but cannot
// select permissions or approve it through the local-only capture path.
// authorize_remote reserves a standalone core dialog. Interactive commands use
// authorize_remote_session with their already-reserved slot and exact session.
func (runtime *tentacleRuntime) authorize_remote(ctx context.Context, id, role string, grants []string) (tentaclewire.SessionEvent, error) {
if runtime == nil || runtime.remotePolicy == nil || !tentaclewire.ValidRoleGrants(role, grants) {
return tentaclewire.SessionEvent{}, tentacle.ErrPermission
}
forms, ok := runtime.forms.(localTentacleForms)
if !ok {
return tentaclewire.SessionEvent{}, tentacle.ErrUnavailable
}
grants = slices.Clone(grants)
runtime.mutex.Lock()
var target *tentacle.Session
if !runtime.disabled {
@@ -44,12 +37,6 @@ func (runtime *tentacleRuntime) authorize_remote(ctx context.Context, id, role s
if target == nil {
return tentaclewire.SessionEvent{}, tentacle.ErrUnavailable
}
installed := target.RemoteGrants()
for _, grant := range grants {
if !slices.Contains(installed, grant) {
return tentaclewire.SessionEvent{}, tentacle.ErrPermission
}
}
// Reserve the interaction before creating a ticket, so a second request
// rejected as busy cannot invalidate the dialog already on screen.
select {
@@ -58,6 +45,32 @@ func (runtime *tentacleRuntime) authorize_remote(ctx context.Context, id, role s
return tentaclewire.SessionEvent{}, tentacle.ErrBusy
}
defer func() { <-runtime.interactionSlots }()
return runtime.authorize_remote_session(ctx, target, role, grants)
}
// Caller owns interactionSlots. A child request may open a dialog, but only
// local capture and the core-issued ticket can authorize the requesting session.
func (runtime *tentacleRuntime) authorize_remote_session(ctx context.Context, target *tentacle.Session, role string, grants []string) (tentaclewire.SessionEvent, error) {
if runtime == nil || runtime.remotePolicy == nil || target == nil || !tentaclewire.ValidRoleGrants(role, grants) {
return tentaclewire.SessionEvent{}, tentacle.ErrPermission
}
forms, ok := runtime.forms.(localTentacleForms)
if !ok {
return tentaclewire.SessionEvent{}, tentacle.ErrUnavailable
}
runtime.mutex.Lock()
current := !runtime.disabled && slices.Contains(runtime.sessions, target)
runtime.mutex.Unlock()
if !current {
return tentaclewire.SessionEvent{}, tentacle.ErrUnavailable
}
grants = slices.Clone(grants)
installed := target.RemoteGrants()
for _, grant := range grants {
if !slices.Contains(installed, grant) {
return tentaclewire.SessionEvent{}, tentacle.ErrPermission
}
}
approval, err := target.BeginRemoteApproval(role, grants)
if err != nil {
return tentaclewire.SessionEvent{}, err
@@ -69,7 +82,7 @@ func (runtime *tentacleRuntime) authorize_remote(ctx context.Context, id, role s
defer stopPolicy()
stopSession := context.AfterFunc(target.OutputContext(), cancel)
defer stopSession()
form := remote_consent_form(id, role, grants)
form := remote_consent_form(target.TentacleID(), role, grants)
replies, err := forms.OpenLocal(ctx, form)
if err != nil {
return tentaclewire.SessionEvent{}, err
+10
View File
@@ -119,6 +119,16 @@ func (session *Session) RevokeRemote() {
session.revoke_remote_locked("local", true)
}
// RevokeRemoteEpoch retires only the action/lease owned by a completed core
// interaction. Closing an old workflow cannot revoke a replacement approval.
func (session *Session) RevokeRemoteEpoch(epoch uint64) {
session.mutex.Lock()
defer session.mutex.Unlock()
if session.remote != nil && session.remote.epoch == epoch {
session.revoke_remote_locked("local", true)
}
}
// RemoteContext is for future reader owners to check again at delivery. A live
// context is necessary, not sufficient: focus, lock and output policy still apply.
func (session *Session) RemoteContext(lease tentaclewire.RemoteLease, grant string) (context.Context, error) {
+48
View File
@@ -123,6 +123,54 @@ func remoteCall(t *testing.T, session *Session, method string, params any) (json
return result.Value, result.Code
}
func TestRemoteWorkflowEpochCleanup(t *testing.T) {
manager, err := NewManager(DefaultLimits(), nil)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := manager.Shutdown(ctx); err != nil {
t.Error(err)
}
})
session := startSessionSDK(t, manager, "go", "workflow")
first, err := session.AuthorizeRemote("controller", []string{"remote-present"})
if err != nil {
t.Fatal(err)
}
second, err := session.AuthorizeRemote("controller", []string{"remote-present"})
if err != nil {
t.Fatal(err)
}
session.RevokeRemoteEpoch(first.Epoch)
raw, code := remoteCall(t, session, "remote.open", tentaclewire.RemoteOpen{Action: second.Action, Epoch: second.Epoch, Role: second.Role, Grants: second.Grants})
var lease tentaclewire.RemoteLease
if code != 0 || json.Unmarshal(raw, &lease) != nil {
t.Fatal("old workflow revoked its replacement")
}
delivery, err := session.RemoteContext(lease, "remote-present")
if err != nil {
t.Fatal(err)
}
session.RevokeRemoteEpoch(second.Epoch)
if delivery.Err() == nil {
t.Fatal("workflow close retained its live lease")
}
// Pending actions also release exclusive ownership on close, before their
// timeout and without requiring a remote.open/remote.close pair.
pending, err := session.AuthorizeRemote("controller", []string{"remote-present"})
if err != nil {
t.Fatal(err)
}
session.RevokeRemoteEpoch(pending.Epoch)
other := startSessionSDK(t, manager, "go", "other-workflow")
if _, err := other.AuthorizeRemote("controlled", []string{"output-observe"}); err != nil {
t.Fatal("closed setup retained exclusive ownership", err)
}
}
func openRemote(t *testing.T, session *Session) tentaclewire.RemoteLease {
t.Helper()
action, err := session.AuthorizeRemote("controlled", []string{"output-observe"})
+14 -1
View File
@@ -25,6 +25,14 @@ type Interaction struct {
Clipboard *string `json:"clipboard,omitempty"`
Done bool `json:"done,omitempty"`
Reader *ReaderRequest `json:"reader,omitempty"`
Remote *RemoteRequest `json:"remote,omitempty"`
}
// RemoteRequest asks the reader to show its own local consent form. It never
// grants access by itself and can target only the requesting process session.
type RemoteRequest struct {
Role string `json:"role"`
Grants []string `json:"grants"`
}
type Submission struct {
@@ -34,6 +42,7 @@ type Submission struct {
Item *Item `json:"item,omitempty"`
Error string `json:"error,omitempty"`
Reader *ReaderState `json:"reader,omitempty"`
Remote *SessionEvent `json:"remote,omitempty"`
}
// Item is a single copied value, without accessible identities or native handles.
@@ -71,7 +80,11 @@ func DecodeInteraction(data []byte) (Interaction, error) {
if value.Source != "" && value.Source != "focus" && value.Source != "clipboard" {
return value, ErrProtocol
}
if value.Reader != nil {
if value.Remote != nil {
if !ValidRoleGrants(value.Remote.Role, value.Remote.Grants) || value.Next == "" || value.Reader != nil || value.Form != nil || value.Source != "" || value.Poll != "" || value.Clipboard != nil || value.Done {
return value, ErrProtocol
}
} else if value.Reader != nil {
if ValidateReaderRequest(*value.Reader) != nil || value.Form != nil || value.Source != "" || value.Poll != "" || value.Clipboard != nil || value.Done || value.Reader.Snapshot && value.Next == "" {
return value, ErrProtocol
}
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: GPL-3.0-or-later
package tentaclewire
import (
"encoding/json"
"testing"
)
func TestRemoteConsentInteractionValidation(t *testing.T) {
valid := Interaction{Remote: &RemoteRequest{Role: "controller", Grants: []string{"remote-present"}}, Next: "approved"}
data, _ := json.Marshal(valid)
if _, err := DecodeInteraction(data); err != nil {
t.Fatal(err)
}
for _, raw := range []string{
`{"remote":null,"next":"approved"}`,
`{"remote":{"role":"controller","grants":["remote-present"]}}`,
`{"remote":{"role":"controller","grants":["keyboard-inject"]},"next":"approved"}`,
`{"remote":{"role":"controlled","grants":[]},"next":"approved"}`,
`{"remote":{"role":"controlled","grants":["output-observe","output-observe"]},"next":"approved"}`,
`{"remote":{"role":"controlled","grants":["output-observe"],"session":"other"},"next":"approved"}`,
} {
if _, err := DecodeInteraction([]byte(raw)); err == nil {
t.Fatal("invalid remote interaction accepted", raw)
}
}
for _, field := range []string{`"clipboard":"text"`, `"source":"clipboard"`, `"done":true`, `"poll":"poll"`, `"reader":{"snapshot":true}`} {
raw := `{"remote":{"role":"controller","grants":["remote-present"]},"next":"approved",` + field + `}`
if _, err := DecodeInteraction([]byte(raw)); err == nil {
t.Fatal("remote consent mixed with another effect", field)
}
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ import "strings"
const Name = "Cthulhu"
const Version = "2026.11.03"
const Version = "2026.11.04"
const Channel = "testing"
var Build = Version