Attach Remote keyboard injection to approved controlled sessions

Local launch and managed startup now start the incoming-key owner for
sessions installed with keyboard-inject, using one input-helper
Supervisor retained for the reader lifetime. Delivery rechecks lock,
XTerm handoff and internal reader UI in addition to lease, grant and
deadline. build-cthulhu.sh and both Arch packages build and install
cthulhu-input-helper; a missing helper leaves keyboard input
unavailable without affecting startup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chase CichorzandClaude Opus 5.5 committed 2026-11-06 16:06:38 -05:00
1 parent c1333c2fdb
commit 27017bef11
13 files changed
+362 -66

No files matched your search

+4 -3
View File
@@ -13,7 +13,7 @@ usage() {
cat <<'EOF'
Usage: build-cthulhu.sh [-o DIR | --output-dir DIR] [-- GO_BUILD_FLAGS...]
Build cthulhu and cthulhu-braille, obtaining a suitable Go toolchain if needed.
Build cthulhu, cthulhu-braille and cthulhu-input-helper, obtaining a suitable Go toolchain if needed.
-o, --output-dir DIR Write both binaries to DIR (default: checkout root).
Relative paths start at the caller's directory.
@@ -56,7 +56,7 @@ if [[ $outputDir != /* ]]; then
outputDir="$PWD/$outputDir"
fi
[[ ! -e $outputDir || -d $outputDir ]] || fail "Output directory is an existing file: $outputDir"
for binary in cthulhu cthulhu-braille; do
for binary in cthulhu cthulhu-braille cthulhu-input-helper; do
[[ ! -d $outputDir/$binary ]] || fail "Binary output path is a directory: $outputDir/$binary"
done
cd -- "$projectDir"
@@ -129,4 +129,5 @@ echo "Using $goVersion"
mkdir -p -- "$outputDir"
"$goCommand" build "${buildFlags[@]}" -o "$outputDir/cthulhu" ./cmd/cthulhu
"$goCommand" build "${buildFlags[@]}" -tags brlapi -o "$outputDir/cthulhu-braille" ./cmd/cthulhu-braille
echo "Cthulhu has been built: $outputDir/cthulhu and $outputDir/cthulhu-braille"
"$goCommand" build "${buildFlags[@]}" -o "$outputDir/cthulhu-input-helper" ./cmd/cthulhu-input-helper
echo "Cthulhu has been built: $outputDir/cthulhu, $outputDir/cthulhu-braille and $outputDir/cthulhu-input-helper"
@@ -84,6 +84,7 @@ build() {
go build -o build/cthulhu ./cmd/cthulhu
go build -tags brlapi -o build/cthulhu-braille ./cmd/cthulhu-braille
go build -o build/cthulhu-input-helper ./cmd/cthulhu-input-helper
}
check() {
@@ -98,6 +99,7 @@ package() {
cd "${_pkgname}"
install -Dm755 build/cthulhu "${pkgdir}/usr/bin/cthulhu"
install -Dm755 build/cthulhu-braille "${pkgdir}/usr/bin/cthulhu-braille"
install -Dm755 build/cthulhu-input-helper "${pkgdir}/usr/bin/cthulhu-input-helper"
install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
install -Dm644 AUTHORS "${pkgdir}/usr/share/doc/${pkgname}/AUTHORS"
}
@@ -80,6 +80,7 @@ build() {
go build -o build/cthulhu ./cmd/cthulhu
go build -tags brlapi -o build/cthulhu-braille ./cmd/cthulhu-braille
go build -o build/cthulhu-input-helper ./cmd/cthulhu-input-helper
}
check() {
@@ -94,6 +95,7 @@ package() {
cd "${pkgname}"
install -Dm755 build/cthulhu "${pkgdir}/usr/bin/cthulhu"
install -Dm755 build/cthulhu-braille "${pkgdir}/usr/bin/cthulhu-braille"
install -Dm755 build/cthulhu-input-helper "${pkgdir}/usr/bin/cthulhu-input-helper"
install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
install -Dm644 AUTHORS "${pkgdir}/usr/share/doc/${pkgname}/AUTHORS"
}
+27 -5
View File
@@ -1,10 +1,10 @@
# Experimental Remote keyboard IPC prerequisite
The protocol 1.1 host recognizes `remote.key`, with public Go types and matching
Python validation. Reader code can explicitly attach a bounded incoming-key
mailbox. No production input owner is attached: ordinary reader sessions return
owner-unavailable, and the external Remote command still discards keyboard
payloads. This is not desktop keyboard control.
Python validation. A bounded incoming-key mailbox is attached in production
for sessions installed with `keyboard-inject`, and the external Remote command
forwards controller keys when keyboard control is approved for the session.
See the production attachment section below; live acceptance is pending.
## Wire contract
@@ -116,7 +116,7 @@ original-epoch cleanup/process-exit barrier; unconfirmed retirement blocks
replacement. The quarantined `app.run_remote_keyboard` worker now joins this
supervisor to the authenticated mailbox with a required policy callback; see
[its lifecycle and native evidence](testing/remote-input-helper.md#quarantined-leasemailbox-worker).
No production reader launch or mailbox owner is attached.
`start_remote_keyboard` is now its production caller; see below.
No capture event API, production Xorg input owner or supervised helper attachment,
core escape/bypass integration or installed Remote keyboard command is
@@ -130,3 +130,25 @@ existing speech regression harness, affected-package vet, a separate reader
build and Tentacles `make check`. The lifecycle regression now checks malformed
typed keys and keeps its unsupported-operation check on Braille. No full reader
suite or native/hardware acceptance was performed; no active reader was replaced.
## Production attachment (2026-11-06)
Local launch and managed-session startup now call `start_remote_keyboard`
for every session installed with `keyboard-inject`, next to the speech owner.
It attaches the mailbox before the child becomes active and runs the existing
lease/mailbox worker with one `remoteinput.Supervisor` retained for the reader
lifetime, so the uncertainty latch survives session replacement. The helper is
`cthulhu-input-helper` beside the reader executable; `build-cthulhu.sh` and both
Arch packages now build and install it. A missing helper or a session without
the grant leaves `remote.key` owner-unavailable and does not affect startup.
Delivery-time policy adds reader lock, XTerm-handoff suspension and active
internal reader UI to the worker's lease, grant and deadline checks; a denied
key revokes the original lease. A clean helper failure retires only that lease
and is announced; unconfirmed cleanup is announced as requiring a reader
restart. Real SDK-process tests cover delivery and lock-policy retirement,
handoff denial before helper launch, and the no-grant and missing-helper cases.
This makes injection reachable from an approved controlled session. It does not
add physical-collision safety, injection provenance, lock/AltGr policy or live
acceptance, which remain required before keyboard control is declared ready.
+3 -3
View File
@@ -5,7 +5,7 @@ its own process. `internal/remoteinput` supplies bounded private IPC, a separate
EOF lifeline, and a three-second heartbeat watchdog. The helper owns its X
connection and stored destination-key ledger on one locked OS thread.
No production reader owner launches this helper or attaches `remote.key`.
At this increment, no production reader owner launched this helper or attached `remote.key`. (Superseded 2026-11-06: see [production attachment](../tentacle-remote-keyboard.md#production-attachment-2026-11-06).)
Remote's UI still offers experimental speech only and discards keyboard traffic.
The ordinary build/install script still builds the reader and Braille helper;
this additional executable is source-built by the private test harness only.
@@ -164,12 +164,12 @@ and Control on a separate X connection before fresh-helper admission.
This launcher is callable only as a quarantined prerequisite. Production reader
startup does not construct it, the mailbox has no production native owner, and
the normal build/install script still does not install the input helper. No native
delivery-time policy or physical collision safety is implied.
delivery-time policy or physical collision safety is implied. (Superseded 2026-11-06: see [production attachment](../tentacle-remote-keyboard.md#production-attachment-2026-11-06).)
### Quarantined lease/mailbox worker
`internal/app.run_remote_keyboard` now joins an explicitly enabled reader key
mailbox to a retained Supervisor. It has no production caller. One worker owns
mailbox to a retained Supervisor. At this increment it had no production caller. (Superseded 2026-11-06: see [production attachment](../tentacle-remote-keyboard.md#production-attachment-2026-11-06).) One worker owns
one session's mailbox and serially sends its transitions; it creates no extra
key queue. Host queued/in-flight budgets remain reserved until helper operation
completion or the full cleanup barrier, including uncertain effects.
+4 -3
View File
@@ -13,9 +13,10 @@ import (
"git.stormux.org/storm/cthulhu/internal/tentaclewire"
)
// run_remote_keyboard is a quarantined mailbox owner, with no production caller.
// The caller must retain one supervisor across sessions, including its permanent
// uncertainty latch, and provide native focus/security policy without native I/O.
// run_remote_keyboard is the incoming-key mailbox owner. start_remote_keyboard
// is its production caller; it retains one supervisor across sessions,
// including its permanent uncertainty latch, and provides native focus/security
// policy without native I/O.
// Policy invalidation must cancel ctx or the lease, including while idle. This
// does not establish physical collision safety or trusted command routing.
// Ending the owner disables its session, leaving no accepting orphan mailbox.
@@ -0,0 +1,90 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"context"
"errors"
"os"
"path/filepath"
"slices"
"git.stormux.org/storm/cthulhu/internal/remoteinput"
"git.stormux.org/storm/cthulhu/internal/tentacle"
)
const remoteInputHelperName = "cthulhu-input-helper"
// remote_input_supervisor returns the one Supervisor retained for the reader
// lifetime, so its permanent uncertainty latch survives session replacement.
// A missing helper leaves keyboard injection unavailable rather than failing
// reader or Tentacle startup.
func (runtime *tentacleRuntime) remote_input_supervisor() *remoteinput.Supervisor {
runtime.mutex.Lock()
defer runtime.mutex.Unlock()
if runtime.remoteInput != nil || runtime.remoteInputChecked {
return runtime.remoteInput
}
runtime.remoteInputChecked = true
path := runtime.remoteInputHelper
if path == "" {
executable, err := os.Executable()
if err != nil {
return nil
}
path = filepath.Join(filepath.Dir(executable), remoteInputHelperName)
}
info, err := os.Stat(path)
if err != nil || !info.Mode().IsRegular() || info.Mode().Perm()&0o111 == 0 {
runtime.logger.Info("remote keyboard helper unavailable", "helper", remoteInputHelperName)
return nil
}
supervisor, err := remoteinput.NewSupervisor(path)
if err != nil {
return nil
}
runtime.remoteInput = supervisor
return supervisor
}
// start_remote_keyboard attaches the production incoming-key owner for a
// session installed with keyboard-inject. Like speech, the mailbox exists
// before the child becomes active; a session without the grant or a reader
// without the helper keeps remote.key owner-unavailable. Delivery-time policy
// rechecks lock, handoff suspension and internal reader UI, in addition to the
// worker's lease, grant and deadline checks.
func (runtime *tentacleRuntime) start_remote_keyboard(ctx context.Context, session *tentacle.Session) <-chan struct{} {
done := make(chan struct{})
owner := runtime.output
if owner == nil || owner.gate == nil || runtime.remotePolicy == nil ||
!slices.Contains(session.RemoteGrants(), "keyboard-inject") {
close(done)
return done
}
supervisor := runtime.remote_input_supervisor()
if supervisor == nil {
close(done)
return done
}
allow := func(delivery tentacle.RemoteKeyDelivery) bool {
owner.gate.mutex.Lock()
defer owner.gate.mutex.Unlock()
return !owner.gate.Locked() && !owner.gate.Suspended() && !owner.gate.InternalUIActive()
}
go func() {
defer close(done)
err := run_remote_keyboard(ctx, session, supervisor, allow)
if err == nil || ctx.Err() != nil {
return
}
if errors.Is(err, remoteinput.ErrUncertain) {
runtime.logger.Warn("remote keyboard cleanup unconfirmed")
runtime.report("Remote keyboard control stopped and cannot restart until Cthulhu restarts. Check for stuck keys.")
return
}
runtime.report("Remote keyboard control stopped. Connect again to retry.")
}()
return done
}
@@ -0,0 +1,151 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"context"
"encoding/json"
"errors"
"log/slog"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/tentacle"
"git.stormux.org/storm/cthulhu/internal/tentaclewire"
)
func keyboard_attach_runtime(policy *tentacle.RemotePolicy, helper string) *tentacleRuntime {
return &tentacleRuntime{logger: slog.New(slog.DiscardHandler), remotePolicy: policy, remoteInputHelper: helper,
output: &tentacleOutput{gate: &focusGate{}}, announcements: make(chan string, 1)}
}
func keyboard_attach_key(t *testing.T, session *tentacle.Session, lease tentaclewire.RemoteLease, sequence uint64, name string, pressed bool) error {
t.Helper()
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
request := tentaclewire.RemoteKeyRequest{RemoteOperation: tentaclewire.RemoteOperation{Lease: lease.Lease, Epoch: lease.Epoch, Sequence: sequence}, Key: tentaclewire.RemoteKey{Name: name, Pressed: pressed}}
data, _ := json.Marshal(map[string]any{"Method": "remote.key", "Params": request})
pending, err := session.Invoke(ctx, "rpc", data)
if err != nil {
t.Fatal(err)
}
select {
case result := <-pending:
return result.Err
case <-ctx.Done():
t.Fatal("keyboard peer call timed out")
return nil
}
}
func keyboard_attach_open(t *testing.T, session *tentacle.Session) tentaclewire.RemoteLease {
t.Helper()
approval, err := session.BeginRemoteApproval("controlled", []string{"keyboard-inject"})
if err != nil {
t.Fatal(err)
}
if _, err := session.AuthorizeRemoteWithApproval(approval, "controlled", []string{"keyboard-inject"}); err != nil {
t.Fatal(err)
}
var lease tentaclewire.RemoteLease
if json.Unmarshal(keyboard_owner_invoke(t, session, "open", nil), &lease) != nil || lease.Epoch == 0 {
t.Fatal("keyboard peer failed to open lease")
}
return lease
}
func TestRemoteKeyboardProductionAttachDeliversAndRetires(t *testing.T) {
policy := tentacle.NewRemotePolicy()
program, log := keyboard_owner_helper(t)
session := keyboard_owner_session(t, policy, []string{"keyboard-inject", "output-observe"})
runtime := keyboard_attach_runtime(policy, program)
ctx, cancel := context.WithCancel(t.Context())
done := runtime.start_remote_keyboard(ctx, session)
t.Cleanup(func() {
cancel()
select {
case <-done:
case <-time.After(6 * time.Second):
t.Error("production keyboard owner did not join")
}
})
lease := keyboard_attach_open(t, session)
if err := keyboard_attach_key(t, session, lease, 1, "a", true); err != nil {
t.Fatal(err)
}
if err := keyboard_attach_key(t, session, lease, 2, "a", false); err != nil {
t.Fatal(err)
}
f := &keyboardOwnerFixture{log: log}
prefix := strconv.FormatUint(lease.Epoch, 10) + " "
f.wait(t, func() bool { return strings.Contains(f.effects(), prefix+"a false\n") })
// Lock/handoff policy revokes the lease and releases the helper.
policy.SetAvailable(false)
f.wait(t, func() bool { return strings.Contains(f.effects(), prefix+"cleanup\n") })
policy.SetAvailable(true)
if runtime.remote_input_supervisor() != runtime.remoteInput || runtime.remoteInput == nil {
t.Fatal("supervisor was not retained for the reader lifetime")
}
}
func TestRemoteKeyboardProductionAttachRechecksReaderUI(t *testing.T) {
policy := tentacle.NewRemotePolicy()
program, log := keyboard_owner_helper(t)
session := keyboard_owner_session(t, policy, []string{"keyboard-inject"})
runtime := keyboard_attach_runtime(policy, program)
runtime.output.gate.suspended.Store(true) // XTerm handoff.
ctx, cancel := context.WithCancel(t.Context())
done := runtime.start_remote_keyboard(ctx, session)
defer func() { cancel(); <-done }()
lease := keyboard_attach_open(t, session)
_ = keyboard_attach_key(t, session, lease, 1, "a", true)
deadline := time.Now().Add(5 * time.Second)
for {
if _, err := session.RemoteContext(lease, "keyboard-inject"); err != nil {
break
}
if time.Now().After(deadline) {
t.Fatal("suspended reader kept keyboard consent")
}
time.Sleep(2 * time.Millisecond)
}
f := &keyboardOwnerFixture{log: log}
if strings.Contains(f.effects(), "a true") || strings.Contains(f.effects(), "starting") {
t.Fatal("suspended reader delivered a remote key", f.effects())
}
}
func TestRemoteKeyboardProductionAttachRequiresGrantAndHelper(t *testing.T) {
policy := tentacle.NewRemotePolicy()
program, _ := keyboard_owner_helper(t)
for name, test := range map[string]struct {
grants []string
helper string
}{
"no grant": {[]string{"output-observe"}, program},
"missing helper": {[]string{"keyboard-inject"}, filepath.Join(t.TempDir(), "absent")},
} {
t.Run(name, func(t *testing.T) {
session := keyboard_owner_session(t, policy, test.grants)
runtime := keyboard_attach_runtime(policy, test.helper)
done := runtime.start_remote_keyboard(t.Context(), session)
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("unavailable keyboard owner did not finish immediately")
}
if name == "missing helper" {
lease := keyboard_attach_open(t, session)
err := keyboard_attach_key(t, session, lease, 1, "a", true)
if err == nil || errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("missing helper accepted a key: %v", err)
}
}
})
}
}
+32 -12
View File
@@ -139,24 +139,29 @@ type keyboardOwnerFixture struct {
log string
}
func keyboard_owner_fixture(t *testing.T, configure ...func(*keyboardOwnerFixture)) *keyboardOwnerFixture {
// keyboard_owner_helper writes the wrapper the real Supervisor launches. The
// wrapper only adapts test-binary arguments; the Supervisor still owns launch,
// private pipes, heartbeats, ACK validation and process reaping.
func keyboard_owner_helper(t *testing.T) (program, log string) {
t.Helper()
executable, err := os.Executable()
if err != nil {
t.Fatal(err)
}
directory := t.TempDir()
f := &keyboardOwnerFixture{log: filepath.Join(directory, "effects"), result: make(chan error, 1), policy: tentacle.NewRemotePolicy()}
f.allowed.Store(true)
t.Setenv("CTHULHU_KEY_OWNER_LOG", f.log)
program := filepath.Join(directory, "helper")
// The wrapper only adapts test-binary arguments. The real Supervisor still
// owns launch, private pipes, heartbeats, ACK validation and process reaping.
log = filepath.Join(directory, "effects")
t.Setenv("CTHULHU_KEY_OWNER_LOG", log)
program = filepath.Join(directory, "helper")
script := "#!/bin/sh\nexec '" + strings.ReplaceAll(executable, "'", "'\\''") + "' -test.run=^TestRemoteKeyboardHelper$ -- \"$@\"\n"
if err := os.WriteFile(program, []byte(script), 0700); err != nil {
t.Fatal(err)
}
f.supervisor, err = remoteinput.NewSupervisor(program)
return program, log
}
func keyboard_owner_session(t *testing.T, policy *tentacle.RemotePolicy, grants []string) *tentacle.Session {
t.Helper()
executable, err := os.Executable()
if err != nil {
t.Fatal(err)
}
@@ -171,21 +176,36 @@ func keyboard_owner_fixture(t *testing.T, configure ...func(*keyboardOwnerFixtur
t.Error(err)
}
})
if err := manager.UseRemotePolicy(f.policy); err != nil {
if err := manager.UseRemotePolicy(policy); err != nil {
t.Fatal(err)
}
f.session, err = manager.Start(t.Context(), tentacle.Spec{ID: "key-owner", Program: []string{executable, "-test.run=^TestRemoteKeyboardPeer$", "--", "remote-keyboard-peer"}, SessionEvents: true, RemoteGrants: []string{"keyboard-inject", "output-observe"}})
session, err := manager.Start(t.Context(), tentacle.Spec{ID: "key-owner", Program: []string{executable, "-test.run=^TestRemoteKeyboardPeer$", "--", "remote-keyboard-peer"}, SessionEvents: true, RemoteGrants: grants})
if err != nil {
t.Fatal(err)
}
select {
case <-f.session.Ready():
case <-session.Ready():
case <-time.After(5 * time.Second):
t.Fatal("keyboard peer did not start")
}
if err := f.session.Activate(); err != nil {
if err := session.Activate(); err != nil {
t.Fatal(err)
}
return session
}
func keyboard_owner_fixture(t *testing.T, configure ...func(*keyboardOwnerFixture)) *keyboardOwnerFixture {
t.Helper()
f := &keyboardOwnerFixture{result: make(chan error, 1), policy: tentacle.NewRemotePolicy()}
f.allowed.Store(true)
program, log := keyboard_owner_helper(t)
f.log = log
var err error
f.supervisor, err = remoteinput.NewSupervisor(program)
if err != nil {
t.Fatal(err)
}
f.session = keyboard_owner_session(t, f.policy, []string{"keyboard-inject", "output-observe"})
for _, setup := range configure {
setup(f)
}
+2 -1
View File
@@ -136,9 +136,10 @@ func (owner *tentacleLifecycle) startPending() {
})
owner.watchers = append(owner.watchers, done)
speechDone := owner.runtime.start_remote_speech(owner.ctx, session)
keysDone := owner.runtime.start_remote_keyboard(owner.ctx, session)
go func() {
defer close(done)
defer func() { <-speechDone }()
defer func() { <-speechDone; <-keysDone }()
select {
case <-session.Revoked():
case <-owner.ctx.Done():
+37 -32
View File
@@ -10,6 +10,7 @@ import (
"time"
"git.stormux.org/storm/cthulhu/internal/input"
"git.stormux.org/storm/cthulhu/internal/remoteinput"
"git.stormux.org/storm/cthulhu/internal/shortcutui"
"git.stormux.org/storm/cthulhu/internal/tentacle"
"git.stormux.org/storm/cthulhu/internal/tentaclewire"
@@ -34,37 +35,40 @@ type tentacleCommand struct {
// native publication; mutex protects dispatch snapshots and immediate disable.
// Only explicitly granted presentation commands reach the core output owner.
type tentacleRuntime struct {
remotePolicy *tentacle.RemotePolicy
readerSettings *preferencesWorker
extensionChanges chan struct{}
forms tentacleForms
focusedItem func(context.Context) (tentaclewire.Item, error)
interactionSlots chan struct{}
registry *tentacle.Registry
requests chan tentacleManagementCall
snapshot tentacle.Snapshot
guardianProgram []string // Private fixture seam; production uses this executable.
output *tentacleOutput
logger *slog.Logger
input tentacleInput
path, parent string
newManager func() (*tentacle.Manager, error) // private fixture seam
fetchRepository func(context.Context, string) (tentacle.Repository, error) // private fixture seam
preparePackage func(context.Context, string, tentacle.Repository, string) (*tentacle.PreparedPackage, error) // private fixture seam
repositoryBusy bool
mutex sync.Mutex
commands map[input.Command]tentacleCommand
sessions []*tentacle.Session
disabled bool
cancel context.CancelFunc
readerContext context.Context
lifecycleContext context.Context
done chan struct{}
failures chan error
announcements chan string
stopError error
results sync.WaitGroup
resultSlots chan struct{}
remotePolicy *tentacle.RemotePolicy
remoteInput *remoteinput.Supervisor // Retained for the reader lifetime.
remoteInputHelper string // Private fixture seam; production uses the reader's directory.
remoteInputChecked bool
readerSettings *preferencesWorker
extensionChanges chan struct{}
forms tentacleForms
focusedItem func(context.Context) (tentaclewire.Item, error)
interactionSlots chan struct{}
registry *tentacle.Registry
requests chan tentacleManagementCall
snapshot tentacle.Snapshot
guardianProgram []string // Private fixture seam; production uses this executable.
output *tentacleOutput
logger *slog.Logger
input tentacleInput
path, parent string
newManager func() (*tentacle.Manager, error) // private fixture seam
fetchRepository func(context.Context, string) (tentacle.Repository, error) // private fixture seam
preparePackage func(context.Context, string, tentacle.Repository, string) (*tentacle.PreparedPackage, error) // private fixture seam
repositoryBusy bool
mutex sync.Mutex
commands map[input.Command]tentacleCommand
sessions []*tentacle.Session
disabled bool
cancel context.CancelFunc
readerContext context.Context
lifecycleContext context.Context
done chan struct{}
failures chan error
announcements chan string
stopError error
results sync.WaitGroup
resultSlots chan struct{}
}
func newTentacleRuntime(logger *slog.Logger, commands commandListener, path, parent string) *tentacleRuntime {
@@ -185,10 +189,11 @@ func (runtime *tentacleRuntime) run(ctx context.Context) error {
runtime.mutex.Unlock()
current, ready = session, session.Ready()
speechDone := runtime.start_remote_speech(ctx, session)
keysDone := runtime.start_remote_keyboard(ctx, session)
watchers.Add(1)
go func() {
defer watchers.Done()
defer func() { <-speechDone }()
defer func() { <-speechDone; <-keysDone }()
select {
case <-session.Revoked():
case <-ctx.Done():
+4 -4
View File
@@ -26,10 +26,10 @@ var (
const operationTimeout = time.Second
const retirementTimeout = Watchdog + time.Second
// Supervisor is a quarantined reader-side process owner. It grants no consent
// and is not attached to the production key mailbox. A future owner must bind
// ctx/epoch to an approved lease and recheck native policy before every Apply.
// Unconfirmed cleanup permanently blocks replacement on this Supervisor.
// Supervisor is the reader-side helper process owner. It grants no consent.
// Its production owner binds ctx/epoch to an approved lease and rechecks
// native policy before every Apply. Unconfirmed cleanup permanently blocks
// replacement on this Supervisor.
type Supervisor struct {
mu sync.Mutex
active bool
+4 -3
View File
@@ -99,16 +99,17 @@ class BuildScriptTests(unittest.TestCase):
self.env["GOTOOLCHAIN"] = "local"
result = self.run_build("--output-dir", "output directory", "--", "-trimpath")
self.assertEqual(result.returncode, 0, result.stderr)
for binary in ("cthulhu", "cthulhu-braille"):
for binary in ("cthulhu", "cthulhu-braille", "cthulhu-input-helper"):
self.assertTrue((self.caller / "output directory" / binary).is_file())
builds = [call for call in self.calls("go") if call[1][0] == "build"]
self.assertEqual(len(builds), 2)
self.assertEqual(len(builds), 3)
for call in builds:
self.assertIn("-trimpath", call[1])
self.assertEqual(call[2], str(self.project))
for name in ("GOTOOLCHAIN", "GOFLAGS", "CGO_CFLAGS", "GOMODCACHE"):
self.assertEqual(call[3][name], self.env[name])
self.assertIn("brlapi", builds[1][1])
self.assertNotIn("brlapi", builds[2][1])
self.assertFalse(self.calls("curl"))
def test_old_go_bootstrap_and_cache_reuse(self):
@@ -120,7 +121,7 @@ class BuildScriptTests(unittest.TestCase):
result = self.run_build()
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(len(self.calls("curl")), 2)
self.assertEqual(len([c for c in self.calls("bootstrap-go") if c[1][0] == "build"]), 4)
self.assertEqual(len([c for c in self.calls("bootstrap-go") if c[1][0] == "build"]), 6)
def test_checksum_failure_never_executes_download(self):
self.env.update(INSTALLED_GO="1.20.14", ARCHIVE_SHA="0" * 64)