lampson 0.2.8 → 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.
- package/.env.example +10 -1
- package/README.md +8 -3
- package/bin/lampson.js +1 -1
- package/chat.syn +111 -7
- package/cli.syn +143 -7
- package/edge.syn +26 -0
- package/hub.tpl.syn +127 -6
- package/lampson.ps1 +43 -2
- package/lampson.sh +40 -2
- package/lib/agents.syn +27 -6
- package/lib/approvals.syn +2 -2
- package/lib/auth.syn +274 -0
- package/lib/computer.syn +2000 -0
- package/lib/computer_judge.syn +410 -0
- package/lib/fs.syn +19 -3
- package/lib/git.syn +2 -2
- package/lib/judge.syn +78 -0
- package/lib/line.syn +2 -2
- package/lib/loop.syn +36 -4
- package/lib/lsp.syn +4 -4
- package/lib/mcp.syn +20 -4
- package/lib/md.syn +11 -11
- package/lib/origin.syn +67 -0
- package/lib/permission.syn +21 -2
- package/lib/prompt.syn +9 -1
- package/lib/provider.syn +70 -20
- package/lib/schedule.syn +12 -12
- package/lib/session.syn +5 -5
- package/lib/settings.syn +65 -1
- package/lib/skills.syn +22 -6
- package/lib/tools/bash.syn +1 -1
- package/lib/tools/common.syn +1 -1
- package/lib/tools/fetch.syn +98 -71
- package/lib/tools/grep.syn +1 -1
- package/lib/tools/memo.syn +3 -3
- package/lib/tools/proc.syn +38 -20
- package/lib/tools/qr.js +26 -0
- package/lib/tools/url.syn +5 -5
- package/lib/tools.syn +23 -2
- package/lib/trace.syn +2 -2
- package/lib/tree.syn +29 -13
- package/lib/update.syn +1 -1
- package/lib/workspaces.syn +202 -36
- package/package.json +3 -2
- package/public/apple-touch-icon.png +0 -0
- package/public/badge-96.png +0 -0
- package/public/css/code.css +48 -0
- package/public/css/hub.css +9 -0
- package/public/css/login.css +16 -0
- package/public/css/panel.css +30 -0
- package/public/css/tokens.css +1 -1
- package/public/hub.html +11 -1
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon-maskable-512.png +0 -0
- package/public/icons-src/badge.svg +12 -0
- package/public/icons-src/icon-maskable.svg +15 -0
- package/public/icons-src/icon.svg +13 -0
- package/public/index.html +14 -3
- package/public/js/agents.js +1 -1
- package/public/js/chat.js +1 -1
- package/public/js/config.js +145 -2
- package/public/js/core.js +4 -2
- package/public/js/devices.js +80 -0
- package/public/js/editor.js +232 -0
- package/public/js/login.js +78 -0
- package/public/js/procs.js +1 -1
- package/public/js/pwa.js +5 -0
- package/public/js/tree.js +12 -4
- package/public/login.html +43 -0
- package/public/manifest.webmanifest +16 -0
- package/public/offline.html +24 -0
- package/public/sw.js +63 -0
- package/public/vendor/highlight.min.js +1343 -0
- package/public/vendor/qrcode.js +2297 -0
- package/skills/computer-use/SKILL.md +178 -0
- package/skills/lampson/SKILL.md +5 -0
- package/web.syn +67 -6
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
|
-
--
|
|
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
|
|
38
|
-
|
|
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
|
-
|
|
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,7 +41,7 @@ let PARALLEL be 4
|
|
|
40
41
|
|
|
41
42
|
export let PROFILES be {
|
|
42
43
|
"build": {
|
|
43
|
-
"tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "fetch", "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
|
},
|
|
@@ -60,7 +61,7 @@ export let PROFILES be {
|
|
|
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", "fetch", "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
|
|
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
|
|
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
|
|
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
|
|
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=" +
|
|
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,
|
|
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"
|