lampson 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/.env.example +10 -1
  2. package/README.md +9 -3
  3. package/bin/lampson.js +1 -1
  4. package/chat.syn +112 -8
  5. package/cli.syn +143 -7
  6. package/edge.syn +26 -0
  7. package/hub.tpl.syn +127 -6
  8. package/lampson.ps1 +43 -2
  9. package/lampson.sh +40 -2
  10. package/lib/agents.syn +30 -9
  11. package/lib/approvals.syn +2 -2
  12. package/lib/auth.syn +274 -0
  13. package/lib/computer.syn +2000 -0
  14. package/lib/computer_judge.syn +410 -0
  15. package/lib/fs.syn +242 -0
  16. package/lib/git.syn +2 -2
  17. package/lib/judge.syn +78 -0
  18. package/lib/line.syn +2 -2
  19. package/lib/loop.syn +41 -8
  20. package/lib/lsp.syn +4 -4
  21. package/lib/mcp.syn +20 -4
  22. package/lib/md.syn +11 -11
  23. package/lib/origin.syn +67 -0
  24. package/lib/permission.syn +47 -2
  25. package/lib/prompt.syn +10 -1
  26. package/lib/provider.syn +70 -20
  27. package/lib/schedule.syn +12 -12
  28. package/lib/session.syn +5 -5
  29. package/lib/settings.syn +65 -1
  30. package/lib/skills.syn +22 -6
  31. package/lib/tools/bash.syn +1 -1
  32. package/lib/tools/common.syn +1 -1
  33. package/lib/tools/fetch.syn +600 -0
  34. package/lib/tools/grep.syn +1 -1
  35. package/lib/tools/memo.syn +3 -3
  36. package/lib/tools/proc.syn +38 -20
  37. package/lib/tools/qr.js +26 -0
  38. package/lib/tools/url.syn +220 -0
  39. package/lib/tools.syn +25 -2
  40. package/lib/trace.syn +2 -2
  41. package/lib/tree.syn +29 -13
  42. package/lib/update.syn +1 -1
  43. package/lib/workspaces.syn +202 -36
  44. package/package.json +12 -3
  45. package/public/apple-touch-icon.png +0 -0
  46. package/public/badge-96.png +0 -0
  47. package/public/css/code.css +48 -0
  48. package/public/css/hub.css +9 -0
  49. package/public/css/login.css +16 -0
  50. package/public/css/panel.css +30 -0
  51. package/public/css/sidebar.css +21 -0
  52. package/public/css/tokens.css +1 -1
  53. package/public/hub.html +11 -1
  54. package/public/icon-192.png +0 -0
  55. package/public/icon-512.png +0 -0
  56. package/public/icon-maskable-512.png +0 -0
  57. package/public/icons-src/badge.svg +12 -0
  58. package/public/icons-src/icon-maskable.svg +15 -0
  59. package/public/icons-src/icon.svg +13 -0
  60. package/public/index.html +14 -3
  61. package/public/js/agents.js +1 -1
  62. package/public/js/chat.js +1 -1
  63. package/public/js/config.js +145 -2
  64. package/public/js/core.js +5 -3
  65. package/public/js/devices.js +80 -0
  66. package/public/js/editor.js +232 -0
  67. package/public/js/login.js +78 -0
  68. package/public/js/procs.js +1 -1
  69. package/public/js/pwa.js +5 -0
  70. package/public/js/tree.js +224 -14
  71. package/public/login.html +43 -0
  72. package/public/manifest.webmanifest +16 -0
  73. package/public/offline.html +24 -0
  74. package/public/sw.js +63 -0
  75. package/public/vendor/highlight.min.js +1343 -0
  76. package/public/vendor/qrcode.js +2297 -0
  77. package/skills/computer-use/SKILL.md +178 -0
  78. package/skills/lampson/SKILL.md +13 -0
  79. package/web.syn +82 -7
package/hub.tpl.syn CHANGED
@@ -8,7 +8,9 @@
8
8
  -- GET /w/<slug>/approve/… links de aprobación de ese workspace
9
9
  -- /api/workspaces… registro, crear (diálogo nativo o explorador), start/stop/quitar, política
10
10
  -- Supervisor: cada 15 s arranca los workspaces que deben vivir y apaga los inactivos (política en ⚙ → idle_hours).
11
- -- Solo loopback puede usar la API del hub (misma regla que web.syn); LAMPSON_WEB_TOKEN para scripts remotos.
11
+ -- Acceso: esta PC entra sin nada; lo remoto — túnel, LAN, VPS — necesita la sesión de un dispositivo emparejado con
12
+ -- passkey (lib/auth.syn), o LAMPSON_WEB_TOKEN para scripts. Qué es «esta PC» y qué origin vale: lib/origin.syn. Las
13
+ -- rutas proxy al API de cada workspace también piden auth: el proceso del workspace confía en lo que le llega del hub.
12
14
 
13
15
  intent: "Lampson hub: workspaces registry, supervisor and reverse proxy to one process per workspace"
14
16
 
@@ -29,34 +31,141 @@ require file.read("hub.tpl.syn")
29
31
  require file("hub.syn")
30
32
  require file.read("public")
31
33
  require file.read("public/*")
34
+ require random
32
35
 
33
36
  use "./lib/workspaces.syn" as ws
34
37
  use "./lib/settings.syn" as settings
38
+ use "./lib/auth.syn" as auth
39
+ use "./lib/origin.syn" as origin
35
40
 
41
+ -- local → {local:true}; remoto con sesión de un dispositivo → {local:false, device}; con LAMPSON_WEB_TOKEN → script
36
42
  task check_client(token, request)
37
- let ip be text(ip of request)
38
- when ip == "127.0.0.1" or ip == "::1" or ip == "::ffff:127.0.0.1"
43
+ let h be headers of request
44
+ -- un navegador en otra origin (CSRF, WebSocket abierto desde otra página, DNS rebinding) no pasa, local o remoto
45
+ when origin.cross_site(h)
46
+ give nothing
47
+ when origin.is_local(text(ip of request), h)
39
48
  give {"local": true}
40
49
  let want be env("LAMPSON_WEB_TOKEN", "")
41
50
  when want != "" and token == want
42
- give {"local": false}
43
- give nothing
51
+ give {"local": false, "device": nothing}
52
+ let s be auth.session_for(get(request.cookies, auth.COOKIE, nothing))
53
+ when s == nothing
54
+ give nothing
55
+ give {"local": false, "device": s["device"]}
56
+
57
+ -- las páginas (no el API): un remoto sin sesión va a /login
58
+ task page_allowed(request)
59
+ give check_client(nothing, request) != nothing
60
+
61
+ task with_session(resp, request, sid)
62
+ give set_cookie(resp, auth.COOKIE, sid, {"max_age": auth.SESSION_MAX_DAYS * 86400, "secure": auth.secure_cookie(headers of request), "same_site": "Lax"})
44
63
 
45
64
  task hub_tick()
46
65
  give ws.tick()
47
66
  cron_every(15, hub_tick)
48
67
 
49
68
  serve on 8080
69
+ -- @bind
70
+ -- túneles (syn tunnel, cloudflared) y el edge de --domain llegan por loopback: así sus X-Forwarded-* llegan y
71
+ -- ip of request es el cliente real (lib/origin.syn)
72
+ trust proxy ["127.0.0.1", "::1"]
50
73
  max_streams 200
51
74
  auth with check_client
52
75
  static "./public"
53
76
 
54
77
  route "GET /"
78
+ when not page_allowed(request)
79
+ give redirect("/login", 302)
55
80
  give html(read_file("public/hub.html"))
56
81
 
82
+ -- ---- acceso remoto: páginas públicas de login/emparejar y su API ----
83
+ route "GET /login"
84
+ give html(read_file("public/login.html"))
85
+ route "GET /pair"
86
+ give html(read_file("public/login.html"))
87
+
88
+ -- ¿quién soy? (la UI decide si muestra «Dispositivos» o «cerrar sesión»)
89
+ route "GET /api/auth/me"
90
+ let u be check_client(nothing, request)
91
+ give {"authenticated": u != nothing, "local": u != nothing and u["local"], "device": when u == nothing then nothing otherwise get(u, "device", nothing), "has_devices": length(auth.devices()) > 0}
92
+
93
+ -- código de emparejamiento para el QR: SOLO desde esta PC
94
+ route "POST /api/auth/pair" requires auth
95
+ when not request.user["local"]
96
+ give fail(403, "el emparejamiento se inicia desde la PC donde corre lampson")
97
+ let p be auth.new_pairing()
98
+ give {"code": p["code"], "expires_in": p["expires_in"], "public_url": auth.public_url()}
99
+
100
+ route "GET /api/auth/public-url" requires auth
101
+ give {"public_url": auth.public_url()}
102
+ route "POST /api/auth/public-url" requires auth
103
+ when not request.user["local"]
104
+ give fail(403, "solo desde la PC")
105
+ expect body {url: text}
106
+ try
107
+ give {"public_url": auth.set_public_url(text((json of request)["url"]))}
108
+ recover err
109
+ give fail(400, text(err))
110
+
111
+ route "POST /api/auth/register/options"
112
+ rate_limit 20 per minute
113
+ expect body {code: text, name: text}
114
+ let b be json of request
115
+ when not auth.pairing_valid(text(b["code"]))
116
+ give fail(400, "el código de emparejamiento venció o ya se usó — generá otro desde la PC")
117
+ give auth.register_options(headers of request, text(b["name"]))
118
+
119
+ route "POST /api/auth/register"
120
+ rate_limit 20 per minute
121
+ let b be json of request
122
+ try
123
+ let r be auth.register(headers of request, get(b, "code", nothing), get(b, "challenge_id", nothing), get(b, "name", ""), get(b, "credential", {}))
124
+ give with_session(ok({"ok": true, "device": r["device"]}), request, r["sid"])
125
+ recover err
126
+ give fail(400, text(err))
127
+
128
+ route "POST /api/auth/login/options"
129
+ rate_limit 30 per minute
130
+ give auth.login_options(headers of request)
131
+
132
+ route "POST /api/auth/login"
133
+ rate_limit 30 per minute
134
+ let b be json of request
135
+ let r be nothing
136
+ try
137
+ set r to auth.login(headers of request, get(b, "challenge_id", nothing), get(b, "credential", {}))
138
+ recover err
139
+ give fail(400, text(err))
140
+ when r == nothing
141
+ give fail(401, "la passkey no coincide con un dispositivo emparejado de este dominio")
142
+ give with_session(ok({"ok": true, "device": r["device"]}), request, r["sid"])
143
+
144
+ route "POST /api/auth/logout"
145
+ when origin.cross_site(headers of request)
146
+ give fail(403, "otra origin")
147
+ let sid be get(request.cookies, auth.COOKIE, nothing)
148
+ when sid != nothing
149
+ auth.drop_session(sid)
150
+ give clear_cookie(ok({"ok": true}), auth.COOKIE)
151
+
152
+ route "GET /api/auth/devices" requires auth
153
+ give {"devices": auth.public_devices(get(request.user, "device", nothing)), "local": request.user["local"], "public_url": auth.public_url()}
154
+
155
+ route "POST /api/auth/devices/remove" requires auth
156
+ expect body {id: text}
157
+ give {"ok": auth.remove_device(text((json of request)["id"]))}
158
+
159
+ route "POST /api/auth/devices/rename" requires auth
160
+ expect body {id: text, name: text}
161
+ let b be json of request
162
+ give {"ok": auth.rename_device(text(b["id"]), text(b["name"]))}
163
+
57
164
  -- la UI de un workspace: el mismo index.html; el frontend deduce /w/<slug> de la URL (BASE en js/core.js).
58
165
  -- Sin barra final: el runtime normaliza "/w/x/" → "/w/x" (301), así que la ruta canónica es esta
59
166
  route "GET /w/:slug"
167
+ when not page_allowed(request)
168
+ give redirect("/login", 302)
60
169
  -- entrar a un workspace apagado lo enciende (y levanta la pausa manual): si alguien abre la URL es para usarlo
61
170
  let w be ws.get(params.slug)
62
171
  when w != nothing and ws.health(w) == nothing
@@ -66,7 +175,19 @@ serve on 8080
66
175
  give html(read_file("public/index.html"))
67
176
 
68
177
  route "GET /api/hub" requires auth
69
- give {"ok": true, "port": ws.HUB_PORT, "idle_hours": ws.idle_hours(), "home": ws.home()}
178
+ give {"ok": true, "port": ws.HUB_PORT, "idle_hours": ws.idle_hours(), "home": ws.home(), "local": request.user["local"]}
179
+
180
+ -- ⚙ → Apagar o reiniciar: Lampson entero (hub, workspaces, edge). Lo hace cli.syn desacoplado, porque este proceso
181
+ -- muere en el medio. Apagar, solo desde esta PC: desde el celular no habría cómo volver a encenderlo
182
+ route "POST /api/hub/shutdown" requires auth
183
+ when not request.user["local"]
184
+ give fail(403, "apagar Lampson solo desde la PC donde corre: desde acá no podrías volver a encenderlo")
185
+ ws.cli_later("hub-stop")
186
+ give {"ok": true}
187
+
188
+ route "POST /api/hub/restart" requires auth
189
+ ws.cli_later("hub-reboot")
190
+ give {"ok": true}
70
191
 
71
192
  route "GET /api/workspaces" requires auth
72
193
  -- la caché del blackboard (tick + acciones) salvo ?fresh=1: la UI lo pide tras encender/apagar
package/lampson.ps1 CHANGED
@@ -1,10 +1,12 @@
1
- # lampson.ps1 — launcher Windows. Cada proyecto es un WORKSPACE con su propio proceso; un hub en :8080 los sirve a todos.
1
+ # lampson.ps1 — launcher Windows. Cada proyecto es un WORKSPACE con su propio proceso; un hub en :8080 los sirve a todos.
2
2
  #
3
3
  # cd C:\mi\proyecto ; lampson # terminal sobre este workspace (lo registra y lo arranca si hace falta)
4
4
  # cd C:\mi\proyecto ; lampson --web # abre http://127.0.0.1:8080/w/<slug>/ en el navegador
5
5
  # lampson --workspace C:\otro\proyecto # elegir la carpeta explícitamente
6
6
  # lampson --hub start|stop|status|logs # el servicio residente (workspaces + tareas programadas)
7
7
  # lampson --install | --uninstall # arrancar el hub al iniciar sesión (Tarea Programada)
8
+ # lampson --domain lampson.midominio.com --email vos@midominio.com # HTTPS con tu dominio (opcional) · --domain off
9
+ # lampson --pair [https://tu-dominio] # QR en la terminal para emparejar el celular
8
10
  # lampson --agent plan · --yolo|--strict|--ask · --update · --help
9
11
  #
10
12
  # Cómo funciona (ver SPEC-WORKSPACES.md): .lampson\ws\<slug>\ es el cwd del proceso del workspace, con una junction
@@ -15,13 +17,16 @@ $here = Split-Path -Parent $MyInvocation.MyCommand.Path
15
17
  $caller = (Get-Location).Path
16
18
 
17
19
  # --- args: acepta --flag y -Flag, sin distinguir mayúsculas ---
18
- $Workspace = ""; $Web = $false; $Agent = ""; $Perm = ""; $Hub = ""; $Install = ""
20
+ $Workspace = ""; $Web = $false; $Agent = ""; $Perm = ""; $Hub = ""; $Install = ""; $Domain = $null; $Email = ""; $Pair = $null
19
21
  $i = 0
20
22
  while ($i -lt $args.Count) {
21
23
  $a = [string]$args[$i]
22
24
  switch -Regex ($a.ToLower()) {
23
25
  '^--?(web|w)$' { $Web = $true }
24
26
  '^--?(hub|daemon|d)$' { $i++; $Hub = if ($i -lt $args.Count) { ([string]$args[$i]).ToLower() } else { "status" } }
27
+ '^--?domain$' { if ($i + 1 -lt $args.Count -and -not ([string]$args[$i + 1]).StartsWith("-")) { $i++; $Domain = [string]$args[$i] } else { $Domain = "" } }
28
+ '^--?email$' { $i++; $Email = [string]$args[$i] }
29
+ '^--?pair$' { if ($i + 1 -lt $args.Count -and -not ([string]$args[$i + 1]).StartsWith("-")) { $i++; $Pair = [string]$args[$i] } else { $Pair = "" } }
25
30
  '^--?install$' { $Install = "install" }
26
31
  '^--?uninstall$' { $Install = "uninstall" }
27
32
  '^--?(workspace|ws)$' { $i++; $Workspace = [string]$args[$i] }
@@ -37,6 +42,12 @@ while ($i -lt $args.Count) {
37
42
  $i++
38
43
  }
39
44
 
45
+ # motor mínimo: el hub y los workspaces declaran `trust proxy` (Synsema 0.6.42+; ver lib/origin.syn). Con uno viejo el
46
+ # hub ni arranca: mejor decirlo acá que dejar un 502
47
+ $MinEngine = [version]"0.6.42"
48
+ $engine = ((& synsema --version 2>$null) -replace '^\D*', '').Trim()
49
+ if (-not ($engine -as [version]) -or [version]$engine -lt $MinEngine) { Write-Error "lampson necesita synsema $MinEngine o más nuevo (tenés '$engine'): synsema update"; exit 1 }
50
+
40
51
  # skills externas globales (npx skills add -g → ~/.agents/skills; Claude Code → ~/.claude/skills), montadas bajo .lampson\
41
52
  # (una capability no puede apuntar a HOME); los workspaces las ven por su junction .lampson\global
42
53
  $skillMounts = @{ "skills-global" = (Join-Path $HOME ".agents\skills"); "skills-claude" = (Join-Path $HOME ".claude\skills") }
@@ -66,9 +77,34 @@ function Invoke-Cli([string]$cmd, [string]$ws) {
66
77
  return ($lines[-1] | ConvertFrom-Json)
67
78
  }
68
79
 
80
+ # synsema escribe UTF-8; PowerShell decodifica lo que CAPTURA (Invoke-Cli) con la página de códigos de la consola
81
+ # (CP850 en Windows en español): «dirección» salía «direcci├│n». UTF-8 mientras corre el script, y se restaura al salir.
82
+ $prevOutEnc = [Console]::OutputEncoding
83
+ try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
69
84
  Push-Location $here
70
85
  try {
71
86
  $env:LAMPSON_HOME = $here
87
+ # --- dominio propio (opcional): lampson --domain [dominio|off] [--email vos@dominio] ---
88
+ if ($null -ne $Domain) {
89
+ $env:LAMPSON_DOMAIN = $Domain; $env:LAMPSON_EMAIL = $Email
90
+ $r = Invoke-Cli "domain" ""
91
+ exit 0
92
+ }
93
+ # --- emparejar un celular desde la terminal (VPS por SSH): lampson --pair [https://tu-dominio] ---
94
+ if ($null -ne $Pair) {
95
+ $env:LAMPSON_PAIR_URL = $Pair
96
+ $r = Invoke-Cli "pair" ""
97
+ if ($r.error) { exit 1 }
98
+ $mins = [math]::Round([double]$r.expires_in / 60)
99
+ Write-Host ""
100
+ Write-Host "Apuntá la cámara del celular a este QR y tocá «Crear passkey» (vale $mins minutos, un solo uso):"
101
+ Write-Host ""
102
+ if (Get-Command node -ErrorAction SilentlyContinue) { node (Join-Path $here "lib\tools\qr.js") $r.link } else { Write-Host " (sin Node no puedo dibujar el QR: abrí el link de abajo en el celular)" }
103
+ Write-Host ""
104
+ Write-Host (" o abrí en el celular: " + $r.link)
105
+ Write-Host (" después entrás con la huella en " + $r.public_url)
106
+ exit 0
107
+ }
72
108
  # --- hub / servicio ---
73
109
  if ($Hub -ne "") {
74
110
  switch ($Hub) {
@@ -116,9 +152,14 @@ try {
116
152
  $env:LAMPSON_WORKSPACE = $target; $env:LAMPSON_WS = $r.slug; $env:LAMPSON_PORT = [string]$r.port
117
153
  if ($Agent -ne "") { $env:LAMPSON_AGENT = $Agent }
118
154
  if ($Perm -ne "") { $env:LAMPSON_PERMISSION = $Perm }
155
+ # juez de computer use (⚙ → Computer use): el runtime lo lee del entorno al arrancar
156
+ if ($r.judge_provider) { $env:SYNSEMA_JUDGE_PROVIDER = $r.judge_provider }
157
+ if ($r.judge_model) { $env:SYNSEMA_JUDGE_MODEL = $r.judge_model }
158
+ if ($r.judge_key) { $env:TYPESAFE_API_KEY = $r.judge_key }
119
159
  $envFile = Join-Path $here ".env"
120
160
  if (Test-Path -LiteralPath $envFile) { synsema run --env-file $envFile chat.syn } else { synsema run chat.syn }
121
161
  } finally { Pop-Location }
122
162
  } finally {
123
163
  Pop-Location
164
+ try { [Console]::OutputEncoding = $prevOutEnc } catch {}
124
165
  }
package/lampson.sh CHANGED
@@ -3,15 +3,20 @@
3
3
  # cd /mi/proyecto && lampson # terminal sobre este workspace
4
4
  # cd /mi/proyecto && lampson --web # abre http://127.0.0.1:8080/w/<slug>/
5
5
  # lampson --hub start|stop|status|logs|restart · --install (systemd --user) · --uninstall
6
+ # lampson --domain lampson.midominio.com --email vos@midominio.com # HTTPS propio (opcional) · --domain off
7
+ # lampson --pair [https://tu-dominio] # QR en la terminal para emparejar el celular
6
8
  # lampson --workspace /ruta [--agent plan] [--yolo|--strict|--ask] · --update
7
9
  set -euo pipefail
8
10
  here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
11
  caller="$(pwd)"
10
- ws=""; web=0; hub=""; install=""
12
+ ws=""; web=0; hub=""; install=""; domain="-"; email=""; pair_url="-"
11
13
  while [ $# -gt 0 ]; do
12
14
  case "$1" in
13
15
  --web) web=1 ;;
14
16
  --hub|--daemon) shift; hub="${1:-status}" ;;
17
+ --domain) if [ $# -gt 1 ] && [ "${2#-}" = "$2" ]; then shift; domain="$1"; else domain=""; fi ;;
18
+ --pair) if [ $# -gt 1 ] && [ "${2#-}" = "$2" ]; then shift; pair_url="$1"; else pair_url=""; fi ;;
19
+ --email) shift; email="${1:-}" ;;
15
20
  --install) install=install ;;
16
21
  --uninstall) install=uninstall ;;
17
22
  --workspace) shift; ws="$1" ;;
@@ -20,12 +25,18 @@ while [ $# -gt 0 ]; do
20
25
  --strict) export LAMPSON_PERMISSION=strict ;;
21
26
  --ask) export LAMPSON_PERMISSION=ask ;;
22
27
  --update) echo "actualizando Lampson en $here"; git -C "$here" pull --ff-only origin main; echo "lampson $(git -C "$here" rev-parse --short HEAD)"; exit $? ;;
23
- -*) echo "uso: lampson [--web] [--hub start|stop|status|logs|restart] [--install|--uninstall] [--workspace RUTA] [--agent PERFIL] [--yolo|--strict|--ask] [--update]" >&2; exit 1 ;;
28
+ -*) echo "uso: lampson [--web] [--hub start|stop|status|logs|restart] [--domain DOMINIO|off [--email EMAIL]] [--pair [URL]] [--install|--uninstall] [--workspace RUTA] [--agent PERFIL] [--yolo|--strict|--ask] [--update]" >&2; exit 1 ;;
24
29
  *) ws="$1" ;;
25
30
  esac
26
31
  shift
27
32
  done
28
33
  export LAMPSON_HOME="$here"
34
+ # motor mínimo: el hub y los workspaces declaran `trust proxy` (Synsema 0.6.42+; ver lib/origin.syn)
35
+ min_engine="0.6.42"
36
+ engine="$(synsema --version 2>/dev/null | sed 's/^[^0-9]*//')"
37
+ if [ -z "$engine" ] || [ "$(printf '%s\n%s\n' "$min_engine" "$engine" | sort -V | head -n1)" != "$min_engine" ]; then
38
+ echo "lampson necesita synsema $min_engine o más nuevo (tenés '${engine:-ninguno}'): synsema update" >&2; exit 1
39
+ fi
29
40
  # skills externas globales, montadas bajo .lampson/ (los workspaces las ven por .lampson/global)
30
41
  mkdir -p "$here/.lampson"
31
42
  for pair in "skills-global:$HOME/.agents/skills" "skills-claude:$HOME/.claude/skills"; do
@@ -45,6 +56,28 @@ cli() { # última línea = JSON
45
56
  echo "$out" | tail -n 1
46
57
  }
47
58
  json() { node -e "const j=JSON.parse(process.argv[1]);const v=j[process.argv[2]];console.log(typeof v==='object'?JSON.stringify(v):v)" "$1" "$2" 2>/dev/null || python3 -c "import json,sys;print(json.loads(sys.argv[1])[sys.argv[2]])" "$1" "$2"; }
59
+ # dominio propio (opcional): HTTPS automático con Let's Encrypt delante del hub
60
+ if [ "$domain" != "-" ]; then
61
+ (cd "$here" && LAMPSON_CMD=domain LAMPSON_DOMAIN="$domain" LAMPSON_EMAIL="$email" synsema run cli.syn | sed '$d')
62
+ exit 0
63
+ fi
64
+ # emparejar un celular desde la terminal (VPS por SSH): lampson --pair [https://tu-dominio]
65
+ if [ "$pair_url" != "-" ]; then
66
+ r="$(cd "$here" && LAMPSON_CMD=pair LAMPSON_PAIR_URL="$pair_url" synsema run cli.syn)" || { echo "$r" >&2; exit 1; }
67
+ echo "$r" | sed '$d'
68
+ r="$(echo "$r" | tail -n 1)"
69
+ link="$(json "$r" link)"
70
+ case "$link" in https://*|http://*) ;; *) exit 1 ;; esac
71
+ secs="$(json "$r" expires_in)"
72
+ echo ""
73
+ echo "Apuntá la cámara del celular a este QR y tocá «Crear passkey» (vale $(( ${secs%.*} / 60 )) minutos, un solo uso):"
74
+ echo ""
75
+ if command -v node >/dev/null 2>&1; then node "$here/lib/tools/qr.js" "$link"; else echo " (sin Node no puedo dibujar el QR: abrí el link de abajo en el celular)"; fi
76
+ echo ""
77
+ echo " o abrí en el celular: $link"
78
+ echo " después entrás con la huella en $(json "$r" public_url)"
79
+ exit 0
80
+ fi
48
81
  if [ -n "$hub" ]; then
49
82
  case "$hub" in
50
83
  start) cli hub-start >/dev/null; echo "hub: http://127.0.0.1:8080" ;;
@@ -92,4 +125,9 @@ if [ "$web" = 1 ]; then
92
125
  fi
93
126
  cd "$here/$dir"
94
127
  export LAMPSON_WORKSPACE="$ws" LAMPSON_WS="$slug" LAMPSON_PORT="$port"
128
+ # juez de computer use (⚙ → Computer use): el runtime lo lee del entorno al arrancar
129
+ jp="$(json "$r" judge_provider)"; jm="$(json "$r" judge_model)"; jk="$(json "$r" judge_key)"
130
+ [ -n "$jp" ] && export SYNSEMA_JUDGE_PROVIDER="$jp"
131
+ [ -n "$jm" ] && export SYNSEMA_JUDGE_MODEL="$jm"
132
+ [ -n "$jk" ] && export TYPESAFE_API_KEY="$jk"
95
133
  if [ -f "$here/.env" ]; then exec synsema run --env-file "$here/.env" chat.syn; else exec synsema run chat.syn; fi
package/lib/agents.syn CHANGED
@@ -33,6 +33,7 @@ use "./provider.syn" as provider
33
33
  use "./prompt.syn" as prompt
34
34
  use "./mcp.syn" as mcp
35
35
  use "./plugins.syn" as plugins
36
+ use "./computer.syn" as computer
36
37
 
37
38
  export let DIR be ".lampson/agents"
38
39
  let MAX_TASKS be 6
@@ -40,27 +41,27 @@ let PARALLEL be 4
40
41
 
41
42
  export let PROFILES be {
42
43
  "build": {
43
- "tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "bash", "process", "memory", "todo", "skill", "mcp", "plugin", "schedule", "delegate"],
44
+ "tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "fetch", "bash", "process", "memory", "todo", "skill", "mcp", "plugin", "schedule", "delegate", "computer_use"],
44
45
  "steps": 40,
45
46
  "addendum": "You are in BUILD mode: you may read, edit, create files and run commands. Delegate exploration of large codebases to the `explore` agent and independent code review to the `review` agent when it saves context; several independent questions can go in one delegate call (they run in parallel)."
46
47
  },
47
48
  "plan": {
48
- "tools": ["read", "ls", "find", "grep", "lsp", "memory", "todo", "skill", "delegate"],
49
+ "tools": ["read", "ls", "find", "grep", "lsp", "fetch", "memory", "todo", "skill", "delegate"],
49
50
  "steps": 30,
50
51
  "addendum": "You are in PLAN mode: READ-ONLY. You cannot edit files or run commands (those tools are not available). Investigate the codebase, then answer with a concrete, numbered implementation plan: files to touch, what changes in each, risks, and how to verify. Do not write code beyond short illustrative snippets."
51
52
  },
52
53
  "review": {
53
- "tools": ["read", "ls", "find", "grep", "lsp", "bash", "process", "memory", "skill"],
54
+ "tools": ["read", "ls", "find", "grep", "lsp", "fetch", "bash", "process", "memory", "skill"],
54
55
  "steps": 25,
55
56
  "addendum": "You are a REVIEWER: read-only on files (no write/edit), but you may run commands (tests, linters, git diff). Find real bugs, missing error handling, security issues and violations of the project's conventions. Report findings as a list with file:line, severity and a suggested fix. Do not modify files."
56
57
  },
57
58
  "explore": {
58
- "tools": ["read", "ls", "find", "grep", "lsp", "memory", "skill"],
59
+ "tools": ["read", "ls", "find", "grep", "lsp", "fetch", "memory", "skill"],
59
60
  "steps": 20,
60
61
  "addendum": "You are an EXPLORER: read-only. Locate the code relevant to the question (files, functions, line numbers) and report it concisely with paths and short excerpts. Do not propose changes."
61
62
  },
62
63
  "worker": {
63
- "tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "bash", "process", "memory", "todo", "skill"],
64
+ "tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "fetch", "bash", "process", "memory", "todo", "skill", "computer_use"],
64
65
  "steps": 40,
65
66
  "addendum": "You are a WORKER sub-agent: implement exactly the scoped task you were given (read, edit, run tests), then report what you changed (files) and how you verified it. Stay inside the scope; if something outside it is needed, report it instead of doing it."
66
67
  }
@@ -93,14 +94,31 @@ export task addendum_for(name)
93
94
  task mcp_readonly_only(name)
94
95
  give not (name == "build" or name == "worker")
95
96
 
97
+ -- tools del perfil que entran de verdad: computer_use está en build/worker pero solo cuando el usuario la
98
+ -- encendió y el driver existe (lib/computer.syn) — si no, el modelo ni la ve
99
+ export task tool_names(p)
100
+ require exec
101
+ require time
102
+ require env("LAMPSON_*")
103
+ require env("OS")
104
+ require file.read(".lampson")
105
+ require file.read(".lampson/*")
106
+ when contains(p["tools"], "computer_use")
107
+ when not computer.available()
108
+ give where(p["tools"], (n) => n != "computer_use")
109
+ give p["tools"]
110
+
96
111
  export task registry_for(name)
112
+ require exec
113
+ require time
97
114
  require file(".lampson")
98
115
  require file(".lampson/*")
99
116
  require file("workspace")
100
117
  require file("workspace/*")
101
118
  require env("LAMPSON_*")
119
+ require env("OS")
102
120
  let p be profile(name)
103
- let reg be tools.registry_subset(p["tools"])
121
+ let reg be tools.registry_subset(tool_names(p))
104
122
  when contains(p["tools"], "delegate")
105
123
  set reg["delegate"] to delegate
106
124
  each m in mcp.names(mcp_readonly_only(name))
@@ -110,13 +128,16 @@ export task registry_for(name)
110
128
  give reg
111
129
 
112
130
  export task catalog_for(name)
131
+ require exec
132
+ require time
113
133
  require file(".lampson")
114
134
  require file(".lampson/*")
115
135
  require file("workspace")
116
136
  require file("workspace/*")
117
137
  require env("LAMPSON_*")
138
+ require env("OS")
118
139
  let p be profile(name)
119
- let cat be tools.catalog_subset(p["tools"])
140
+ let cat be tools.catalog_subset(tool_names(p))
120
141
  when contains(p["tools"], "delegate")
121
142
  set cat to append(cat, DELEGATE_SPEC)
122
143
  give cat + mcp.catalog(mcp_readonly_only(name)) + plugins.catalog(mcp_readonly_only(name))
@@ -155,7 +176,7 @@ task write_json(id, m)
155
176
  write_file(path(id, "json"), json_encode(m))
156
177
 
157
178
  task one_line(s, max)
158
- let t be replace_text(replace_text(text(s), "\r", ""), "\n", " ⏎ ")
179
+ let t be replace(replace(text(s), "\r", ""), "\n", " ⏎ ")
159
180
  when length(t) > max
160
181
  give slice(t, 0, max) + "…"
161
182
  give t
@@ -313,7 +334,7 @@ export task run_child(spec)
313
334
  let system be {"role": "system", "content": prompt.build(env_info(cfg), p["addendum"] + CHILD_NOTE)}
314
335
  let full_brief be when spec["context"] == nothing or spec["context"] == "" then spec["brief"] otherwise spec["brief"] + "\n\nContext from the delegating agent:\n" + spec["context"]
315
336
  let messages be [system, {"role": "user", "content": full_brief}]
316
- let opts be loop.default_opts(tools.registry_subset(p["tools"]), tools.catalog_subset(p["tools"]), nothing)
337
+ let opts be loop.default_opts(tools.registry_subset(tool_names(p)), tools.catalog_subset(tool_names(p)), nothing)
317
338
  set opts to loop.with_steps(opts, p["steps"])
318
339
  set opts to loop.with_mode(opts, when spec["mode"] == "yolo" then "yolo" otherwise "strict")
319
340
  set opts to loop.with_inbox(opts, child_inbox, id)
package/lib/approvals.syn CHANGED
@@ -101,7 +101,7 @@ task notify(meta, links)
101
101
  let headers be {"Content-Type": "application/json", "User-Agent": "lampson"}
102
102
  let sec be secret_text()
103
103
  when sec != ""
104
- set headers["X-Lampson-Signature"] to "sha256=" + hmac_sha256(body, sec)
104
+ set headers["X-Lampson-Signature"] to "sha256=" + decode(hmac(body, sec), "hex")
105
105
  try
106
106
  let r be http_post(url, body, headers)
107
107
  when not r["ok"]
@@ -147,7 +147,7 @@ export task answer_with_token(id, tok, decision)
147
147
  observe "approval:token:" + id as want
148
148
  when want == nothing
149
149
  give {"ok": false, "why": "expired"}
150
- when not verify_hmac(id, hmac_sha256(id, tok), want)
150
+ when not verify_hmac(id, decode(hmac(id, tok), "hex"), want)
151
151
  give {"ok": false, "why": "bad token"}
152
152
  share nothing as "approval:token:" + id
153
153
  let v be when decision then "yes" otherwise "no"