Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion images/chromium-headful/client/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
},
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
},
"remote.containers.defaultExtensions": [
"octref.vetur",
Expand Down
7 changes: 6 additions & 1 deletion images/chromium-headful/client/src/components/video.vue
Original file line number Diff line number Diff line change
Expand Up @@ -463,10 +463,15 @@
if (this.clipboard_write_available) {
try {
await navigator.clipboard.writeText(clipboard)
console.log('[clipboard] system clipboard write successful:', clipboard)
this.$accessor.remote.setClipboard(clipboard)
} catch (err: any) {
this.$log.error(err)
console.error('[clipboard] system clipboard write failed:', err)
this.$accessor.remote.setClipboard(clipboard)
}
} else {
console.log('[clipboard] navigator.clipboard.writeText unavailable, forcing fallback')
this.$accessor.remote.setClipboard(clipboard)
}
}

Expand Down
5 changes: 2 additions & 3 deletions images/chromium-headful/client/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import './assets/styles/main.scss'

import Vue from 'vue'

import Notifications from 'vue-notification'
import ToolTip from 'v-tooltip'
import Logger from './plugins/log'
import Client from './plugins/neko'
import Axios from './plugins/axios'
import Swal from './plugins/swal'
import Anime from './plugins/anime'

import { i18n } from './plugins/i18n'
import store from './store'
import app from './app.vue'
import GlobalPaste from './plugins/globalPaste'

Vue.config.productionTip = false

Expand All @@ -23,6 +21,7 @@ Vue.use(Axios)
Vue.use(Swal)
Vue.use(Anime)
Vue.use(Client)
Vue.use(GlobalPaste)

new Vue({
i18n,
Expand Down
1 change: 1 addition & 0 deletions images/chromium-headful/client/src/neko/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
}

protected [EVENT.CONTROL.CLIPBOARD]({ text }: ControlClipboardPayload) {
console.log('[clipboard] EVENT.CONTROL.CLIPBOARD:', text)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider the security implications of logging clipboard data. Clipboard content could contain sensitive information like passwords, API keys, or personal data. While this logging is helpful for debugging, you may want to either:

  1. Truncate/sanitize the logged text (e.g., console.log('[clipboard] EVENT.CONTROL.CLIPBOARD:', text.substring(0, 50) + '...'))
  2. Add a flag to disable clipboard logging in production
  3. Use a more secure logging method that doesn't expose sensitive data in browser console

This is particularly important since the browser console can be accessed by users or browser extensions.

Type: Security | Severity: High

this.$accessor.remote.setClipboard(text)
}

Expand Down
30 changes: 30 additions & 0 deletions images/chromium-headful/client/src/plugins/globalPaste.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// src/plugins/globalPaste.ts
import { PluginObject } from 'vue'

const GlobalPaste: PluginObject<undefined> = {
install() {
document.addEventListener(
'keydown',
async (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'v') {
e.preventDefault()
console.log(`[vue:globalPaste]:call`)
try {
const text = await navigator.clipboard.readText()
console.log(`[vue:globalPaste]:payload:` , text)
await fetch('http://localhost:10001/computer/paste', {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using localhost here won't work in prod

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on it 👍

note :
idea of external controls (outside of neko events) exposed via API might be useful down the line ; as a controller for agents to stream events to/from via sockets. not via localhost on the instance like in here - would be routed to and token-gated by the kernel api , and connected to by the ^ container and user/agent outside of the vue app

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes definitely--this is why we added the click/move mouse stuff into the API. I think eventually we will go deeper on that front

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Hardcoded URL Causes Production Environment Issues

The http://localhost:10001/computer/paste URL is hardcoded in remote.ts and globalPaste.ts. This prevents the application from functioning correctly in production, containerized, or distributed environments where the API server is not running on localhost:10001. This issue was previously identified in PR discussions.

Locations (2)

Fix in Cursor Fix in Web

method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
})
} catch (err) {
console.error('paste proxy failed', err)
}
}
},
{ capture: true }
)
},
}

export default GlobalPaste
20 changes: 20 additions & 0 deletions images/chromium-headful/client/src/store/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,26 @@ export const mutations = mutationTree(state, {

setClipboard(state, clipboard: string) {
state.clipboard = clipboard
console.log('[clipboard] state updated:', clipboard)

// Always send to local fallback
fetch('http://localhost:10001/computer/paste', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ text: clipboard }),
})
.then((res) => {
console.log('[clipboard] fallback POST status:', res.status)
return res.text()
})
.then((body) => {
console.log('[clipboard] fallback response:', body)
})
.catch((err) => {
console.error('[clipboard] fallback error:', err)
})
},

setKeyboardModifierState(state, { capsLock, numLock, scrollLock }) {
Expand Down
2 changes: 1 addition & 1 deletion server/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,4 @@ DISPLAY_NUM := $(shell \
ffmpeg -f avfoundation -list_devices true -i "" 2>&1 | grep "Capture screen" | head -1 | sed 's/.*\[\([0-9]*\)\].*/\1/' 2>/dev/null || echo "2"; \
else \
echo "0"; \
fi)
fi)
66 changes: 25 additions & 41 deletions server/cmd/api/api/computer.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
// server/cmd/api/api/computer.go
package api

import (
"context"
"fmt"
"os"
"os/exec"
"strconv"

"github.com/onkernel/kernel-images/server/lib/logger"
Expand All @@ -11,64 +14,43 @@ import (

func (s *ApiService) MoveMouse(ctx context.Context, request oapi.MoveMouseRequestObject) (oapi.MoveMouseResponseObject, error) {
log := logger.FromContext(ctx)

// Validate request body
if request.Body == nil {
return oapi.MoveMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "request body is required"}}, nil
}
body := *request.Body

// Ensure non-negative coordinates
if body.X < 0 || body.Y < 0 {
return oapi.MoveMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "coordinates must be non-negative"}}, nil
}

// Build xdotool arguments
args := []string{}

// Hold modifier keys (keydown)
if body.HoldKeys != nil {
for _, key := range *body.HoldKeys {
args = append(args, "keydown", key)
}
}

// Move the cursor to the desired coordinates
args = append(args, "mousemove", "--sync", strconv.Itoa(body.X), strconv.Itoa(body.Y))

// Release modifier keys (keyup)
if body.HoldKeys != nil {
for _, key := range *body.HoldKeys {
args = append(args, "keyup", key)
}
}

log.Info("executing xdotool", "args", args)

output, err := defaultXdoTool.Run(ctx, args...)
if err != nil {
log.Error("xdotool command failed", "err", err, "output", string(output))
return oapi.MoveMouse500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to move mouse"}}, nil
}

return oapi.MoveMouse200Response{}, nil
}

func (s *ApiService) ClickMouse(ctx context.Context, request oapi.ClickMouseRequestObject) (oapi.ClickMouseResponseObject, error) {
log := logger.FromContext(ctx)

// Validate request body
if request.Body == nil {
return oapi.ClickMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "request body is required"}}, nil
}
body := *request.Body

// Ensure non-negative coordinates
if body.X < 0 || body.Y < 0 {
return oapi.ClickMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "coordinates must be non-negative"}}, nil
}

// Map button enum to xdotool button code. Default to left button.
btn := "1"
if body.Button != nil {
buttonMap := map[oapi.ClickMouseRequestButton]string{
Expand All @@ -78,39 +60,27 @@ func (s *ApiService) ClickMouse(ctx context.Context, request oapi.ClickMouseRequ
oapi.Back: "8",
oapi.Forward: "9",
}
var ok bool
btn, ok = buttonMap[*body.Button]
if !ok {
if m, ok := buttonMap[*body.Button]; ok {
btn = m
} else {
return oapi.ClickMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: fmt.Sprintf("unsupported button: %s", *body.Button)}}, nil
}
}

// Determine number of clicks (defaults to 1)
numClicks := 1
if body.NumClicks != nil && *body.NumClicks > 0 {
numClicks = *body.NumClicks
}

// Build xdotool arguments
args := []string{}

// Hold modifier keys (keydown)
if body.HoldKeys != nil {
for _, key := range *body.HoldKeys {
args = append(args, "keydown", key)
}
}

// Move the cursor
args = append(args, "mousemove", "--sync", strconv.Itoa(body.X), strconv.Itoa(body.Y))

// click type defaults to click
clickType := oapi.Click
if body.ClickType != nil {
clickType = *body.ClickType
}

// Perform the click action
switch clickType {
case oapi.Down:
args = append(args, "mousedown", btn)
Expand All @@ -125,21 +95,35 @@ func (s *ApiService) ClickMouse(ctx context.Context, request oapi.ClickMouseRequ
default:
return oapi.ClickMouse400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: fmt.Sprintf("unsupported click type: %s", clickType)}}, nil
}

// Release modifier keys (keyup)
if body.HoldKeys != nil {
for _, key := range *body.HoldKeys {
args = append(args, "keyup", key)
}
}

log.Info("executing xdotool", "args", args)

output, err := defaultXdoTool.Run(ctx, args...)
if err != nil {
log.Error("xdotool command failed", "err", err, "output", string(output))
return oapi.ClickMouse500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to execute mouse action"}}, nil
}

return oapi.ClickMouse200Response{}, nil
}

func (s *ApiService) PasteClipboard(ctx context.Context, request oapi.PasteClipboardRequestObject) (oapi.PasteClipboardResponseObject, error) {
log := logger.FromContext(ctx)
if request.Body == nil || request.Body.Text == "" {
return oapi.PasteClipboard400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "text is required"}}, nil
}
text := request.Body.Text
clip := exec.Command("bash", "-c", fmt.Sprintf("printf %%s %q | xclip -selection clipboard", text))
clip.Env = append(os.Environ(), fmt.Sprintf("DISPLAY=%s", defaultXdoTool.display))
if out, err := clip.CombinedOutput(); err != nil {
log.Error("failed to set clipboard", "err", err, "output", string(out))
return oapi.PasteClipboard500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to set clipboard"}}, nil
}
if _, err := defaultXdoTool.Run(ctx, "key", "ctrl+v"); err != nil {
log.Error("failed to paste text", "err", err)
return oapi.PasteClipboard500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to paste text"}}, nil
}
return oapi.PasteClipboard200Response{}, nil
}
Loading
Loading