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.
- package/.env.example +10 -1
- package/README.md +9 -3
- package/bin/lampson.js +1 -1
- package/chat.syn +112 -8
- 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 +30 -9
- 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 +242 -0
- package/lib/git.syn +2 -2
- package/lib/judge.syn +78 -0
- package/lib/line.syn +2 -2
- package/lib/loop.syn +41 -8
- 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 +47 -2
- package/lib/prompt.syn +10 -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 +600 -0
- 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 +220 -0
- package/lib/tools.syn +25 -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 +12 -3
- 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/sidebar.css +21 -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 +5 -3
- 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 +224 -14
- 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 +13 -0
- package/web.syn +82 -7
package/lib/git.syn
CHANGED
|
@@ -21,12 +21,12 @@ export task status()
|
|
|
21
21
|
let branch be ""
|
|
22
22
|
let changes be {}
|
|
23
23
|
let counts be {"modified": 0, "added": 0, "deleted": 0, "untracked": 0}
|
|
24
|
-
each line in split(
|
|
24
|
+
each line in split(replace(out, "\r", ""), "\n")
|
|
25
25
|
when starts_with(line, "## ")
|
|
26
26
|
set branch to split(split(slice(line, 3, length(line)), "...")[0], " ")[0]
|
|
27
27
|
otherwise when length(line) > 3
|
|
28
28
|
let xy be slice(line, 0, 2)
|
|
29
|
-
let path be
|
|
29
|
+
let path be replace(trim(slice(line, 3, length(line))), "\"", "")
|
|
30
30
|
when contains(path, " -> ")
|
|
31
31
|
set path to split(path, " -> ")[1]
|
|
32
32
|
let code be trim(xy)
|
package/lib/judge.syn
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
-- lib/judge.syn — el juez de decisión de Lampson: un modelo System One que contesta preguntas tipadas con
|
|
2
|
+
-- probabilidades (el bloque `judge` de Synsema: whether / choose / rate), no con texto
|
|
3
|
+
--
|
|
4
|
+
-- Es una credencial/configuración de Lampson, como el proveedor del LLM — no de una tool. Hoy lo usan:
|
|
5
|
+
-- * computer_use: la acción `act` (elige el próximo control de una lista cerrada) y page_select (elige la opción
|
|
6
|
+
-- de un desplegable cuando el texto no coincide) — lib/computer.syn + lib/computer_judge.syn
|
|
7
|
+
-- y cualquier parte de Lampson que necesite una decisión calibrada y barata puede usarlo con `require judge` y
|
|
8
|
+
-- judge.on() como compuerta (clasificar, rutear, validar, decidir si algo aplica…).
|
|
9
|
+
--
|
|
10
|
+
-- Backends (se elige en ⚙ → Juez de decisión, /judge en la terminal):
|
|
11
|
+
-- jev TypeSafe Jev, en la nube: rápido (~0,3 s por decisión), multilingüe, necesita API key (keys.typesafe)
|
|
12
|
+
-- laya Laya local (ModernBERT, ~840 MB en disco): sin red ni costo, ~1-2 s por decisión en CPU; medido
|
|
13
|
+
-- 2026-09-28: con interfaces en español el checkpoint `english` se equivoca con confianza alta
|
|
14
|
+
-- off sin juez
|
|
15
|
+
-- El runtime lee el proveedor del ENTORNO al arrancar el proceso (settings.judge_env): cambiarlo reinicia el
|
|
16
|
+
-- workspace (web) o pide salir y volver a entrar (terminal).
|
|
17
|
+
|
|
18
|
+
use "./settings.syn" as settings
|
|
19
|
+
|
|
20
|
+
export let MODES be ["off", "jev", "laya"]
|
|
21
|
+
|
|
22
|
+
export task norm_mode(v)
|
|
23
|
+
let t be lower(trim(text(when v == nothing then "" otherwise v)))
|
|
24
|
+
when t == "jev" or t == "typesafe"
|
|
25
|
+
give "jev"
|
|
26
|
+
when t == "laya" or t == "local"
|
|
27
|
+
give "laya"
|
|
28
|
+
give "off"
|
|
29
|
+
|
|
30
|
+
-- el modo configurado (config.json / .env), esté vivo o no en este proceso
|
|
31
|
+
export task mode()
|
|
32
|
+
require file.read(".lampson")
|
|
33
|
+
require file.read(".lampson/*")
|
|
34
|
+
require env("LAMPSON_*")
|
|
35
|
+
give norm_mode(settings.judge_setting()["mode"])
|
|
36
|
+
|
|
37
|
+
-- encendido en la configuración Y con un proveedor vivo en este proceso: la compuerta de quien lo use
|
|
38
|
+
export task on()
|
|
39
|
+
require file.read(".lampson")
|
|
40
|
+
require file.read(".lampson/*")
|
|
41
|
+
require env("LAMPSON_*")
|
|
42
|
+
when mode() == "off"
|
|
43
|
+
give false
|
|
44
|
+
give judge_available()
|
|
45
|
+
|
|
46
|
+
-- {mode, model_setting, has_key, live, provider_model, restart, from_env}: lo que ven la UI y /judge
|
|
47
|
+
export task status()
|
|
48
|
+
require file.read(".lampson")
|
|
49
|
+
require file.read(".lampson/*")
|
|
50
|
+
require env("LAMPSON_*")
|
|
51
|
+
let js be settings.judge_setting()
|
|
52
|
+
let m be norm_mode(js["mode"])
|
|
53
|
+
let live be judge_available()
|
|
54
|
+
let pm be judge_model()
|
|
55
|
+
-- el proceso corre otro proveedor que el configurado (o ninguno): hace falta reiniciar
|
|
56
|
+
let restart be false
|
|
57
|
+
when m != "off" and not live
|
|
58
|
+
set restart to true
|
|
59
|
+
when m == "laya" and pm != nothing and not starts_with(text(pm), "laya:")
|
|
60
|
+
set restart to true
|
|
61
|
+
when m == "jev" and pm != nothing and starts_with(text(pm), "laya:")
|
|
62
|
+
set restart to true
|
|
63
|
+
give {"mode": m, "model_setting": text(js["model"]), "has_key": settings.raw_key("typesafe") != "", "live": live, "provider_model": when pm == nothing then "" otherwise text(pm), "restart": restart, "from_env": js["from_env"]}
|
|
64
|
+
|
|
65
|
+
-- guardar el modo (y opcionalmente el modelo / la key de TypeSafe) → {ok, error}
|
|
66
|
+
export task configure(m, model, key)
|
|
67
|
+
require file(".lampson")
|
|
68
|
+
require file(".lampson/*")
|
|
69
|
+
require env("LAMPSON_*")
|
|
70
|
+
let nm be lower(trim(text(m)))
|
|
71
|
+
when not contains(MODES, nm) and nm != "typesafe" and nm != "local"
|
|
72
|
+
give {"ok": false, "error": "judge: off, jev or laya"}
|
|
73
|
+
set nm to norm_mode(nm)
|
|
74
|
+
when key != nothing
|
|
75
|
+
when trim(text(key)) != ""
|
|
76
|
+
settings.set_key("typesafe", trim(text(key)))
|
|
77
|
+
settings.set_judge(nm, model)
|
|
78
|
+
give {"ok": true, "error": nothing}
|
package/lib/line.syn
CHANGED
|
@@ -37,7 +37,7 @@ task starts(s, p)
|
|
|
37
37
|
task candidates(buf, ctx)
|
|
38
38
|
when not starts(buf, "/") or contains(buf, "\n")
|
|
39
39
|
give {"kind": "", "items": [], "token": ""}
|
|
40
|
-
let sp be
|
|
40
|
+
let sp be regex_capture(buf, "^(/\S*)\s+(.*)$")
|
|
41
41
|
when sp == nothing
|
|
42
42
|
-- comando a medias: recientes primero cuando es "/" pelado
|
|
43
43
|
let items be []
|
|
@@ -208,7 +208,7 @@ export task read(prompt, color, ctx)
|
|
|
208
208
|
set result to nothing
|
|
209
209
|
set done to true
|
|
210
210
|
otherwise when ev["type"] == "paste"
|
|
211
|
-
set st to insert(st,
|
|
211
|
+
set st to insert(st, replace(ev["text"], "\r", ""))
|
|
212
212
|
set st to refresh_menu(st, ctx)
|
|
213
213
|
otherwise when ev["type"] == "resize"
|
|
214
214
|
ui.set_cols(ev["cols"])
|
package/lib/loop.syn
CHANGED
|
@@ -27,6 +27,23 @@ use "./plugins.syn" as plugins
|
|
|
27
27
|
use "./tools/todo.syn" as todo
|
|
28
28
|
|
|
29
29
|
let MAX_ERRORS_PER_TURN be 8
|
|
30
|
+
-- capturas de computer_use (lib/computer.syn): el driver las deja en el blackboard "computer:screenshot" y acá van al
|
|
31
|
+
-- modelo como imagen adjunta a un mensaje de usuario (un tool result es texto en todos los proveedores). Solo la más
|
|
32
|
+
-- reciente viaja con la imagen; las anteriores quedan como nota (≈1500 tokens cada una, en CADA request).
|
|
33
|
+
let SHOT_PREFIX be "[harness] Screenshot from computer_use: "
|
|
34
|
+
|
|
35
|
+
export task drop_old_screenshots(msgs)
|
|
36
|
+
let out be []
|
|
37
|
+
each m in msgs
|
|
38
|
+
let is_shot be false
|
|
39
|
+
when m["role"] == "user"
|
|
40
|
+
when contains(m, "images")
|
|
41
|
+
set is_shot to starts_with(text(m["content"]), SHOT_PREFIX)
|
|
42
|
+
when is_shot
|
|
43
|
+
set out to append(out, {"role": "user", "content": text(m["content"]) + " [image removed to save context]"})
|
|
44
|
+
otherwise
|
|
45
|
+
set out to append(out, m)
|
|
46
|
+
give out
|
|
30
47
|
-- Conciencia de progreso (2026-08-28, la mejor idea de cada harness — ver notes/*.md):
|
|
31
48
|
-- * SPILL (deepseek): un resultado > SPILL_CAP se guarda entero en .lampson/spill/<call>.txt y el modelo ve
|
|
32
49
|
-- cabeza + cola + "Full result saved to …"; `read` queda exento (ya pagina con offset/limit).
|
|
@@ -47,7 +64,7 @@ let REPEAT_HARD be 8
|
|
|
47
64
|
-- seguidas sin ninguna acción (edit/write/bash/process/delegate/mcp). Visto 2026-08-28 con deepseek-v4-pro:
|
|
48
65
|
-- 39 read/ls/find seguidos, 0 ediciones, 419k tokens, dos veces, con las reglas de prompt ignoradas.
|
|
49
66
|
-- A la mitad del tope el resultado lleva un aviso; al tope, read/ls/find/grep se rechazan hasta que actúe.
|
|
50
|
-
let READ_ONLY_TOOLS be ["read", "ls", "find", "grep"]
|
|
67
|
+
let READ_ONLY_TOOLS be ["read", "ls", "find", "grep", "fetch"]
|
|
51
68
|
let ACTION_TOOLS be ["edit", "write", "bash", "process", "delegate"]
|
|
52
69
|
|
|
53
70
|
export task explore_cap()
|
|
@@ -67,7 +84,7 @@ export task explore_verdict(streak, name, cap)
|
|
|
67
84
|
when n >= cap
|
|
68
85
|
give {"streak": n, "note": "", "refuse": true}
|
|
69
86
|
when n == floor(cap / 2)
|
|
70
|
-
give {"streak": n, "note": "\n\n[harness] " + text(n) + " read-only calls in a row without changing anything. You likely know enough: edit/write now, or run the relevant command. After " + text(cap) + " read-only calls in a row, read/ls/find/grep are refused until you act. If the codebase is genuinely too big, delegate ONE focused question to an `explore` sub-agent instead of reading everything yourself.", "refuse": false}
|
|
87
|
+
give {"streak": n, "note": "\n\n[harness] " + text(n) + " read-only calls in a row without changing anything. You likely know enough: edit/write now, or run the relevant command. After " + text(cap) + " read-only calls in a row, read/ls/find/grep/fetch are refused until you act. If the codebase is genuinely too big, delegate ONE focused question to an `explore` sub-agent instead of reading everything yourself.", "refuse": false}
|
|
71
88
|
give {"streak": n, "note": "", "refuse": false}
|
|
72
89
|
|
|
73
90
|
-- JSON canónico (claves ordenadas en profundidad): reordenar propiedades no engaña al detector
|
|
@@ -98,7 +115,8 @@ task safe_id(id, step)
|
|
|
98
115
|
export task spill(name, id, out)
|
|
99
116
|
require file(".lampson")
|
|
100
117
|
require file(".lampson/*")
|
|
101
|
-
|
|
118
|
+
-- read y fetch se recortan solas (fetch: cabeza+cola con el texto completo ya en .lampson/spill)
|
|
119
|
+
when name == "read" or name == "fetch" or length(out) <= SPILL_CAP
|
|
102
120
|
give out
|
|
103
121
|
-- el informe de los subagentes es el entregable: no se manda a disco (hasta 60k)
|
|
104
122
|
when name == "delegate" and length(out) <= SPILL_CAP * 6
|
|
@@ -106,7 +124,7 @@ export task spill(name, id, out)
|
|
|
106
124
|
let path be SPILL_DIR + "/" + id + ".txt"
|
|
107
125
|
write_file(path, out)
|
|
108
126
|
let half be floor(SPILL_CAP / 2)
|
|
109
|
-
give slice(out, 0, half) + "\n\n[... " + text(length(out) - SPILL_CAP) + " chars omitted. Full result saved to " + path + " — grep
|
|
127
|
+
give slice(out, 0, half) + "\n\n[... " + text(length(out) - SPILL_CAP) + " chars omitted. Full result saved to " + path + " — use the grep or read TOOLS on that exact path (it lives in lampson, outside your workspace: bash cannot see it); do not re-run the command ...]\n\n" + slice(out, length(out) - half, length(out))
|
|
110
128
|
|
|
111
129
|
-- recibo de una línea para un tool result viejo
|
|
112
130
|
export task receipt(m)
|
|
@@ -115,13 +133,13 @@ export task receipt(m)
|
|
|
115
133
|
give out
|
|
116
134
|
let status be "ok"
|
|
117
135
|
when starts_with(out, "ERROR") or starts_with(out, "DENIED")
|
|
118
|
-
set status to slice(
|
|
136
|
+
set status to slice(replace(split(out, "\n")[0], "\r", ""), 0, 120)
|
|
119
137
|
otherwise
|
|
120
|
-
let codes be
|
|
138
|
+
let codes be regex_find_all(out, "\\[exit code [0-9]+\\]")
|
|
121
139
|
when length(codes) > 0
|
|
122
140
|
let last be codes[length(codes) - 1]
|
|
123
141
|
set status to slice(last, 1, length(last) - 1)
|
|
124
|
-
let saved be
|
|
142
|
+
let saved be regex_find_all(out, "Full result saved to [^ ]+")
|
|
125
143
|
let where_full be when length(saved) > 0 then " · " + saved[0] otherwise ""
|
|
126
144
|
give "[" + text(m["name"]) + " result pruned: " + status + " · " + text(length(split(out, "\n"))) + " lines/" + text(length(out)) + " chars" + where_full + " — re-run the tool only if you really need the content again]"
|
|
127
145
|
|
|
@@ -323,6 +341,8 @@ export task run_turn(cfg, messages, opts, on_event)
|
|
|
323
341
|
let max_steps be opts["max_steps"]
|
|
324
342
|
let budget be opts["budget_tokens"]
|
|
325
343
|
let tag be opts["tag"]
|
|
344
|
+
-- ¿este modelo acepta imágenes? lo leen las tools que producen capturas (computer_use) para avisar al modelo
|
|
345
|
+
share provider.supports_vision(cfg) as "lampson:vision"
|
|
326
346
|
-- compaction preventiva
|
|
327
347
|
when estimate_tokens(msgs) > opts["compact_at_tokens"]
|
|
328
348
|
emit(on_event, "compact", {"before": estimate_tokens(msgs)}, tag)
|
|
@@ -375,6 +395,11 @@ export task run_turn(cfg, messages, opts, on_event)
|
|
|
375
395
|
emit(on_event, "usage", {"step": steps, "usage": r["usage"], "total": usage}, tag)
|
|
376
396
|
let clean_text be strip_raw_tool_calls(r["text"])
|
|
377
397
|
let assistant be {"role": "assistant", "content": clean_text, "tool_calls": r["tool_calls"]}
|
|
398
|
+
-- razonamiento del paso (solo lo trae el wire para los presets glm): va en el mensaje para
|
|
399
|
+
-- devolvérselo al modelo en el paso siguiente (thinking conservado, ver provider.chat_openai)
|
|
400
|
+
when contains(r, "reasoning")
|
|
401
|
+
when r["reasoning"] != ""
|
|
402
|
+
set assistant["reasoning"] to r["reasoning"]
|
|
378
403
|
set msgs to append(msgs, assistant)
|
|
379
404
|
when clean_text != ""
|
|
380
405
|
emit(on_event, "assistant", clean_text, tag)
|
|
@@ -394,7 +419,7 @@ export task run_turn(cfg, messages, opts, on_event)
|
|
|
394
419
|
set out to "ERROR: you already called " + tc["name"] + " with these exact arguments " + text(repeats) + " times in a row. The result will not change; try a different approach or report the problem."
|
|
395
420
|
emit(on_event, "tool_denied", {"call": tc, "reason": "repeated call (" + text(repeats) + "x)"}, tag)
|
|
396
421
|
otherwise when ex["refuse"]
|
|
397
|
-
set out to "ERROR: exploration cap reached — " + text(explore_streak) + " read-only calls in a row (read/ls/find/grep) without a single change. Reading more will not help. Do one of: (1) edit/write the files you already read; (2) run a command (bash/process) that moves the task; (3) delegate ONE focused question to an `explore` sub-agent; (4) tell the user what you need. read/ls/find/grep are refused until you do."
|
|
422
|
+
set out to "ERROR: exploration cap reached — " + text(explore_streak) + " read-only calls in a row (read/ls/find/grep) without a single change. Reading more will not help. Do one of: (1) edit/write the files you already read; (2) run a command (bash/process) that moves the task; (3) delegate ONE focused question to an `explore` sub-agent; (4) tell the user what you need. read/ls/find/grep/fetch are refused until you do."
|
|
398
423
|
emit(on_event, "tool_denied", {"call": tc, "reason": "exploration cap (" + text(explore_streak) + " read-only calls without acting)"}, tag)
|
|
399
424
|
otherwise when contains(READ_ONLY_TOOLS, tc["name"]) and reads_total >= turn_cap
|
|
400
425
|
set out to "ERROR: this turn already made " + text(reads_total) + " read-only calls (limit " + text(turn_cap) + "). Everything you read is in your context. Act on it now (edit/write/run), delegate ONE focused question to an `explore` sub-agent, or report to the user."
|
|
@@ -421,6 +446,14 @@ export task run_turn(cfg, messages, opts, on_event)
|
|
|
421
446
|
set errors to errors + 1
|
|
422
447
|
emit(on_event, "tool_result", {"call": tc, "output": out}, tag)
|
|
423
448
|
set msgs to append(msgs, tool_result_msg(tc, out))
|
|
449
|
+
-- captura de pantalla pendiente (computer_use): adjunta como imagen si el proveedor tiene visión
|
|
450
|
+
observe "computer:screenshot" as shot
|
|
451
|
+
when shot != nothing
|
|
452
|
+
share nothing as "computer:screenshot"
|
|
453
|
+
when provider.supports_vision(cfg)
|
|
454
|
+
set msgs to drop_old_screenshots(msgs)
|
|
455
|
+
set msgs to append(msgs, {"role": "user", "content": SHOT_PREFIX + shot["label"] + ". Look at it, then continue the task.", "images": shot["images"]})
|
|
456
|
+
emit(on_event, "inbox", "captura de pantalla adjunta al modelo (" + shot["label"] + ")", tag)
|
|
424
457
|
when errors >= MAX_ERRORS_PER_TURN
|
|
425
458
|
set msgs to append(msgs, {"role": "user", "content": "[harness] Too many tool errors this turn. Stop calling tools and report what happened."})
|
|
426
459
|
emit(on_event, "busy", {"label": "pensando", "kind": "llm"}, tag)
|
package/lib/lsp.syn
CHANGED
|
@@ -92,13 +92,13 @@ export task servers()
|
|
|
92
92
|
-- resuelven realpaths, así que el rootUri y los uri de los documentos tienen que ser los reales.
|
|
93
93
|
export task root_path()
|
|
94
94
|
require env("LAMPSON_*")
|
|
95
|
-
let r be
|
|
95
|
+
let r be replace(env("LAMPSON_WORKSPACE", "workspace"), "\\", "/")
|
|
96
96
|
when c.ends_with(r, "/")
|
|
97
97
|
set r to slice(r, 0, length(r) - 1)
|
|
98
98
|
give r
|
|
99
99
|
|
|
100
100
|
export task to_uri(path)
|
|
101
|
-
let p be
|
|
101
|
+
let p be replace(replace(path, "\\", "/"), " ", "%20")
|
|
102
102
|
when matches(p, "[A-Za-z]:/.*")
|
|
103
103
|
give "file:///" + p
|
|
104
104
|
give "file://" + p
|
|
@@ -109,7 +109,7 @@ export task from_uri(uri)
|
|
|
109
109
|
set u to slice(u, 8, length(u))
|
|
110
110
|
otherwise when starts_with(u, "file://")
|
|
111
111
|
set u to slice(u, 7, length(u))
|
|
112
|
-
give
|
|
112
|
+
give replace(replace(replace(u, "%3A", ":"), "%3a", ":"), "%20", " ")
|
|
113
113
|
|
|
114
114
|
-- uri del server → ruta relativa al workspace (o absoluta si cae fuera)
|
|
115
115
|
export task relativize(uri, root)
|
|
@@ -123,7 +123,7 @@ export task relativize(uri, root)
|
|
|
123
123
|
give p
|
|
124
124
|
|
|
125
125
|
task ext_of(path)
|
|
126
|
-
let parts be split(
|
|
126
|
+
let parts be split(replace(path, "\\", "/"), "/")
|
|
127
127
|
let base be parts[length(parts) - 1]
|
|
128
128
|
let segs be split(base, ".")
|
|
129
129
|
when length(segs) < 2
|
package/lib/mcp.syn
CHANGED
|
@@ -109,6 +109,24 @@ agent McpSup
|
|
|
109
109
|
let sub be bus_subscribe(["mcp.req." + name, "mcp.stop." + name, "mcp.stop_all"])
|
|
110
110
|
-- request síncrona (handshake y tools/list): manda y espera la respuesta con ese id, saltando
|
|
111
111
|
-- notificaciones y basura; devuelve {result} | {error} | nothing (timeout / proceso muerto)
|
|
112
|
+
-- MIGA runtime (2026-09-06): proc_spawn en line_mode parte las líneas de más de 1 MiB en trozos de 1 MiB; un
|
|
113
|
+
-- resultado grande (una captura base64) llegaba en trozos que no decodificaban y la respuesta se perdía.
|
|
114
|
+
-- `acc` junta los trozos hasta que el JSON cierra; un trozo suelto que decodifica solo reinicia el acumulador.
|
|
115
|
+
let acc be ""
|
|
116
|
+
task decode_line(data)
|
|
117
|
+
let joined be acc + data
|
|
118
|
+
try
|
|
119
|
+
let m be json_decode(joined)
|
|
120
|
+
set acc to ""
|
|
121
|
+
give m
|
|
122
|
+
recover e1
|
|
123
|
+
try
|
|
124
|
+
let m2 be json_decode(data)
|
|
125
|
+
set acc to ""
|
|
126
|
+
give m2
|
|
127
|
+
recover e2
|
|
128
|
+
set acc to when length(joined) > 40000000 then "" otherwise joined
|
|
129
|
+
give nothing
|
|
112
130
|
task rpc(method, params, timeout)
|
|
113
131
|
set seq to seq + 1
|
|
114
132
|
let id be seq
|
|
@@ -122,12 +140,10 @@ agent McpSup
|
|
|
122
140
|
otherwise when ev["type"] == "exit"
|
|
123
141
|
set answer to {"error": "server exited with code " + text(ev["data"]["exit_code"])}
|
|
124
142
|
otherwise when ev["type"] == "stdout"
|
|
125
|
-
|
|
126
|
-
|
|
143
|
+
let m be decode_line(ev["data"])
|
|
144
|
+
when m != nothing
|
|
127
145
|
when contains(m, "id") and m["id"] == id
|
|
128
146
|
set answer to when contains(m, "error") then {"error": text(m["error"]["message"])} otherwise {"result": m["result"]}
|
|
129
|
-
recover e2
|
|
130
|
-
set answer to answer
|
|
131
147
|
give answer
|
|
132
148
|
task notify(method, params)
|
|
133
149
|
proc_send(p, json_encode({"jsonrpc": "2.0", "method": method, "params": params}) + "\n")
|
package/lib/md.syn
CHANGED
|
@@ -21,9 +21,9 @@ task width_now()
|
|
|
21
21
|
task paths(s, color)
|
|
22
22
|
when not color
|
|
23
23
|
give s
|
|
24
|
-
let out be
|
|
25
|
-
set out to
|
|
26
|
-
set out to
|
|
24
|
+
let out be regex_replace(s, "(^|[\s(])([\w.-]*/[\w./-]{2,})", "\1" + ESC + "[34m\2" + ESC + "[0m")
|
|
25
|
+
set out to regex_replace(out, "(^|[\s(])(\w+\.(syn|js|ts|tsx|jsx|json|md|py|rs|go|ps1|sh|html|css|toml|yml|yaml|env))(\b)", "\1" + ESC + "[34m\2" + ESC + "[0m")
|
|
26
|
+
set out to regex_replace(out, "(^|[\s(])(\w+\(\))", "\1" + ESC + "[34m\2" + ESC + "[0m")
|
|
27
27
|
give out
|
|
28
28
|
|
|
29
29
|
task sgr(color, code, s)
|
|
@@ -42,16 +42,16 @@ task rep(ch, n)
|
|
|
42
42
|
task emphasis(s, color)
|
|
43
43
|
let out be s
|
|
44
44
|
-- links [texto](url) → texto url (antes que la cursiva: la url puede traer _ o *)
|
|
45
|
-
set out to
|
|
46
|
-
set out to
|
|
47
|
-
set out to
|
|
48
|
-
set out to
|
|
45
|
+
set out to regex_replace(out, "\[([^\]]+)\]\(([^)]+)\)", sgr(color, "4", "\1") + sgr(color, "2", " \2"))
|
|
46
|
+
set out to regex_replace(out, "\*\*([^*]+)\*\*", sgr(color, "1", "\1"))
|
|
47
|
+
set out to regex_replace(out, "__([^_]+)__", sgr(color, "1", "\1"))
|
|
48
|
+
set out to regex_replace(out, "~~([^~]+)~~", sgr(color, "9", "\1"))
|
|
49
49
|
-- cursiva: *x* solo si no está pegada a texto/número (2*3 no es cursiva). Dos pasadas:
|
|
50
50
|
-- el grupo \3 consume el separador, así que "*a* *b*" necesita la segunda.
|
|
51
51
|
let pass be 0
|
|
52
52
|
while pass < 2
|
|
53
|
-
set out to
|
|
54
|
-
set out to
|
|
53
|
+
set out to regex_replace(out, "(^|[^\w*])\*([^*\s][^*]*?)\*($|[^\w*])", "\1" + sgr(color, "3", "\2") + "\3")
|
|
54
|
+
set out to regex_replace(out, "(^|[^\w_])_([^_\s][^_]*?)_($|[^\w_])", "\1" + sgr(color, "3", "\2") + "\3")
|
|
55
55
|
set pass to pass + 1
|
|
56
56
|
give out
|
|
57
57
|
|
|
@@ -162,8 +162,8 @@ export task render(md, color)
|
|
|
162
162
|
let table be []
|
|
163
163
|
each line in split(md, "\n")
|
|
164
164
|
let t be trim(line)
|
|
165
|
-
let heading be
|
|
166
|
-
let item be
|
|
165
|
+
let heading be regex_capture(t, "^(#{1,6}) (.+)$")
|
|
166
|
+
let item be regex_capture(t, "^(\d{1,3})[.)] (.*)$")
|
|
167
167
|
let is_row be starts_with(t, "|") and ends_with(t, "|") and not in_code
|
|
168
168
|
when length(table) > 0 and not is_row
|
|
169
169
|
each l in table_lines(table, pre, color)
|
package/lib/origin.syn
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
-- lib/origin.syn — de dónde viene una petición HTTP: la usan el hub (hub.tpl.syn, lib/auth.syn) y cada workspace (web.syn).
|
|
2
|
+
--
|
|
3
|
+
-- «Local» (esta PC, entra sin passkey) se PRUEBA con las tres cosas a la vez, nunca se deduce de lo que falta:
|
|
4
|
+
-- 1. el peer es loopback;
|
|
5
|
+
-- 2. no pasó por ningún proxy (sin X-Forwarded-*, Forwarded ni los headers de cloudflared);
|
|
6
|
+
-- 3. el Host CRUDO es un nombre de loopback. Un túnel (syn tunnel, cloudflared, ngrok) o el edge de --domain
|
|
7
|
+
-- traen el Host público; una página con DNS rebinding trae el suyo.
|
|
8
|
+
-- Desde el motor v0.6.42 los X-Forwarded-* de un peer que no está en `trust proxy` se borran antes del programa. El
|
|
9
|
+
-- hub y los workspaces confían en loopback (`trust proxy` / `--trust-proxy`): ahí conectan los túneles, el edge y el
|
|
10
|
+
-- proxy del hub, así que lo que dicen llega y `ip of request` es el cliente real. Sin eso, un syn tunnel entraba
|
|
11
|
+
-- como «esta PC» (2026-10-05). Un túnel TCP crudo (ssh -R, ngrok tcp) no deja rastro y no se puede distinguir:
|
|
12
|
+
-- nunca se apunta uno al hub.
|
|
13
|
+
--
|
|
14
|
+
-- Otra origin en el navegador — CSRF, un WebSocket abierto desde otra página, DNS rebinding — se rechaza siempre,
|
|
15
|
+
-- local o remoto: un navegador manda Origin en todo POST, en todo WebSocket y en los fetch entre origins, y tiene
|
|
16
|
+
-- que coincidir con la origin de la propia petición. Sin Origin (curl, scripts, navegación) no hay sitio ajeno.
|
|
17
|
+
-- Módulo puro: sin require.
|
|
18
|
+
|
|
19
|
+
let FORWARD_HEADERS be ["x-forwarded-for", "x-forwarded-host", "x-forwarded-proto", "forwarded", "x-real-ip", "cf-connecting-ip", "cf-ray"]
|
|
20
|
+
let LOOPBACK_NAMES be ["localhost", "127.0.0.1", "::1"]
|
|
21
|
+
|
|
22
|
+
export task is_loopback(ip)
|
|
23
|
+
give ip == "127.0.0.1" or ip == "::1" or ip == "::ffff:127.0.0.1"
|
|
24
|
+
|
|
25
|
+
-- valor de Host (o X-Forwarded-Host) → nombre sin puerto ni corchetes, en minúsculas
|
|
26
|
+
export task host_name(value)
|
|
27
|
+
let h be trim(split(lower(text(value)), ",")[0])
|
|
28
|
+
when starts_with(h, "[")
|
|
29
|
+
give split(slice(h, 1, length(h)), "]")[0]
|
|
30
|
+
give split(h, ":")[0]
|
|
31
|
+
|
|
32
|
+
-- "host[:puerto]" como lo vio el navegador: el que puso el proxy de confianza, o el Host si no hubo proxy
|
|
33
|
+
task authority(headers)
|
|
34
|
+
give trim(split(lower(text(get(headers, "x-forwarded-host", get(headers, "host", "")))), ",")[0])
|
|
35
|
+
|
|
36
|
+
-- el Host que llegó a ESTE servidor (un proxy lo reescribe al suyo; el de afuera queda en X-Forwarded-Host)
|
|
37
|
+
export task raw_host(headers)
|
|
38
|
+
give host_name(get(headers, "host", ""))
|
|
39
|
+
|
|
40
|
+
-- el dominio que ve el usuario: rp_id de WebAuthn
|
|
41
|
+
export task public_host(headers)
|
|
42
|
+
give host_name(authority(headers))
|
|
43
|
+
|
|
44
|
+
-- la origin que ve el navegador. Lampson no termina TLS en el hub ni en los workspaces: sin un proxy de confianza
|
|
45
|
+
-- que diga https (el edge, un túnel), es http
|
|
46
|
+
export task origin_of(headers)
|
|
47
|
+
let proto be lower(trim(split(text(get(headers, "x-forwarded-proto", "")), ",")[0]))
|
|
48
|
+
when proto == ""
|
|
49
|
+
set proto to "http"
|
|
50
|
+
give proto + "://" + authority(headers)
|
|
51
|
+
|
|
52
|
+
export task forwarded(headers)
|
|
53
|
+
each h in FORWARD_HEADERS
|
|
54
|
+
when contains(headers, h)
|
|
55
|
+
give true
|
|
56
|
+
give false
|
|
57
|
+
|
|
58
|
+
export task loopback_host(headers)
|
|
59
|
+
give contains(LOOPBACK_NAMES, raw_host(headers))
|
|
60
|
+
|
|
61
|
+
export task cross_site(headers)
|
|
62
|
+
when not contains(headers, "origin")
|
|
63
|
+
give false
|
|
64
|
+
give lower(trim(text(headers["origin"]))) != origin_of(headers)
|
|
65
|
+
|
|
66
|
+
export task is_local(ip, headers)
|
|
67
|
+
give is_loopback(ip) and not forwarded(headers) and loopback_host(headers)
|
package/lib/permission.syn
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
--
|
|
10
10
|
-- Modos (LAMPSON_PERMISSION): "ask" (default) | "yolo" (dangerous → allow) | "strict" (dangerous → deny)
|
|
11
11
|
|
|
12
|
+
use "./tools/url.syn" as u
|
|
13
|
+
use "./computer.syn" as computer
|
|
14
|
+
|
|
12
15
|
-- Tier 1: nunca. Coincidencia por substring, case-insensitive.
|
|
13
16
|
-- (el comando se evalúa con un espacio final añadido, así "rm -rf / " matchea la raíz pero NO "rm -rf /tmp/x")
|
|
14
17
|
export let HARDLINE be [
|
|
@@ -69,9 +72,30 @@ export task evaluate(name, args, mode)
|
|
|
69
72
|
give {"decision": "deny", "reason": "strict mode (dangerous: " + danger + ")"}
|
|
70
73
|
give {"decision": "ask", "reason": "dangerous pattern: " + danger}
|
|
71
74
|
give {"decision": "allow", "reason": "command"}
|
|
75
|
+
when name == "fetch"
|
|
76
|
+
-- política de hosts de url.syn (hermes url_safety): secretos en la URL y metadata de la nube se
|
|
77
|
+
-- deniegan SIEMPRE (incluso en yolo); hosts privados/loopback (el dev server del usuario) piden;
|
|
78
|
+
-- lo público se permite. Una URL inválida se permite: la tool devuelve el error explicado.
|
|
79
|
+
let furl be when contains(args, "url") then text(args["url"]) otherwise ""
|
|
80
|
+
let why be u.sensitive(furl)
|
|
81
|
+
when why != nothing
|
|
82
|
+
give {"decision": "deny", "reason": "the URL carries " + why + " — secrets never travel in URLs"}
|
|
83
|
+
let fp be u.parse(furl)
|
|
84
|
+
when not fp["ok"]
|
|
85
|
+
give {"decision": "allow", "reason": "invalid URL (the tool explains)"}
|
|
86
|
+
let cls be u.host_class(fp["host"])
|
|
87
|
+
when cls == "blocked"
|
|
88
|
+
give {"decision": "deny", "reason": "cloud metadata endpoint " + fp["host"]}
|
|
89
|
+
when cls == "private"
|
|
90
|
+
when mode == "yolo"
|
|
91
|
+
give {"decision": "allow", "reason": "yolo mode (private host " + fp["host"] + ")"}
|
|
92
|
+
when mode == "strict"
|
|
93
|
+
give {"decision": "deny", "reason": "strict mode (private host " + fp["host"] + ")"}
|
|
94
|
+
give {"decision": "ask", "reason": "fetches a private/loopback host (" + fp["origin"] + ")"}
|
|
95
|
+
give {"decision": "allow", "reason": "public URL"}
|
|
72
96
|
when name == "write" or name == "edit"
|
|
73
97
|
-- el scope file("./*") ya impide salir del workspace; aquí solo miramos secretos obvios
|
|
74
|
-
let p be when contains(args, "path") then lower(
|
|
98
|
+
let p be when contains(args, "path") then lower(replace(text(args["path"]), "\\", "/")) otherwise ""
|
|
75
99
|
let parts be split(p, "/")
|
|
76
100
|
let base be parts[length(parts) - 1]
|
|
77
101
|
when is_secret_env(base)
|
|
@@ -130,11 +154,27 @@ export task evaluate(name, args, mode)
|
|
|
130
154
|
when mode == "strict"
|
|
131
155
|
give {"decision": "deny", "reason": "strict mode (installs a third-party skill)"}
|
|
132
156
|
give {"decision": "ask", "reason": "installs a third-party skill from " + (when contains(args, "source") then text(args["source"]) otherwise "?") + " (" + (when contains(args, "scope") then text(args["scope"]) otherwise "global") + ")"}
|
|
157
|
+
when name == "computer_use"
|
|
158
|
+
-- el escritorio del usuario (lib/computer.syn): leer (ventanas, árbol, capturas, texto) siempre; actuar pide UNA
|
|
159
|
+
-- sola vez por corrida — la primera acción — y cubre todo lo demás (teclas en foreground, abrir apps y
|
|
160
|
+
-- navegadores): pedir por cada cosa hacía inútil la tool (2026-09-06: cuatro pedidos para llenar un form).
|
|
161
|
+
-- La tool deja "computer:grant" en el blackboard al ejecutar la primera aprobada. strict deniega actuar.
|
|
162
|
+
let cact be when contains(args, "action") then lower(text(args["action"])) otherwise ""
|
|
163
|
+
when not contains(computer.ACTING, cact)
|
|
164
|
+
give {"decision": "allow", "reason": "read-only tool"}
|
|
165
|
+
when mode == "yolo"
|
|
166
|
+
give {"decision": "allow", "reason": "yolo mode (computer " + cact + ")"}
|
|
167
|
+
when mode == "strict"
|
|
168
|
+
give {"decision": "deny", "reason": "strict mode (computer " + cact + ")"}
|
|
169
|
+
observe "computer:grant" as granted
|
|
170
|
+
when granted == true
|
|
171
|
+
give {"decision": "allow", "reason": "desktop control granted this run"}
|
|
172
|
+
give {"decision": "ask", "reason": "takes control of your desktop for the rest of this run — clicks and keys in the background (briefly to the front when an app needs it), opening apps and browser windows; asked once"}
|
|
133
173
|
give {"decision": "allow", "reason": "read-only tool"}
|
|
134
174
|
|
|
135
175
|
-- Resumen legible de una tool call, para mostrar al humano antes de aprobar / en el log.
|
|
136
176
|
task one_line(s, max)
|
|
137
|
-
let t be
|
|
177
|
+
let t be replace(replace(s, "\r", ""), "\n", " ⏎ ")
|
|
138
178
|
when length(t) > max
|
|
139
179
|
give slice(t, 0, max) + "…"
|
|
140
180
|
give t
|
|
@@ -142,6 +182,8 @@ task one_line(s, max)
|
|
|
142
182
|
export task describe_call(name, args)
|
|
143
183
|
when starts_with(name, "mcp_") or starts_with(name, "plugin_")
|
|
144
184
|
give name + " " + one_line(json_encode(args), 160)
|
|
185
|
+
when name == "computer_use"
|
|
186
|
+
give computer.describe(args)
|
|
145
187
|
when name == "lsp" and contains(args, "op") and text(args["op"]) == "add"
|
|
146
188
|
give "lsp add " + (when contains(args, "server") then text(args["server"]) otherwise "?") + " (" + (when contains(args, "scope") then text(args["scope"]) otherwise "global") + ")"
|
|
147
189
|
when name == "lsp"
|
|
@@ -167,6 +209,9 @@ export task describe_call(name, args)
|
|
|
167
209
|
give "skill install " + text(args["source"]) + " --skill " + text(args["name"]) + " (" + (when contains(args, "scope") then text(args["scope"]) otherwise "global") + ")"
|
|
168
210
|
when name == "bash"
|
|
169
211
|
give "$ " + one_line(when contains(args, "command") then text(args["command"]) otherwise "", 160)
|
|
212
|
+
when name == "fetch"
|
|
213
|
+
let ffmt be arg(args, "format", "markdown")
|
|
214
|
+
give "fetch " + one_line(arg(args, "url", "?"), 140) + (when ffmt != "markdown" then " (" + ffmt + ")" otherwise "")
|
|
170
215
|
-- lectura/búsqueda: como lo escribiría un humano en la shell
|
|
171
216
|
when name == "read"
|
|
172
217
|
let rng be when contains(args, "offset") or contains(args, "limit") then " (" + (when contains(args, "offset") then "desde " + text(floor(number(text(args["offset"])))) otherwise "") + (when contains(args, "limit") then " " + text(floor(number(text(args["limit"])))) + " líneas" otherwise "") + ")" otherwise ""
|
package/lib/prompt.syn
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
use "./skills.syn" as skills
|
|
8
8
|
use "./tools/memo.syn" as memo
|
|
9
|
+
use "./computer.syn" as computer
|
|
9
10
|
|
|
10
11
|
let CONTEXT_FILES be ["AGENTS.md", "LAMPSON.md"]
|
|
11
12
|
|
|
@@ -22,13 +23,20 @@ task read_context_files()
|
|
|
22
23
|
give out
|
|
23
24
|
|
|
24
25
|
export task build(env_info, addendum)
|
|
26
|
+
require exec
|
|
27
|
+
require time
|
|
25
28
|
require file.read("workspace")
|
|
26
29
|
require file.read("workspace/*")
|
|
27
30
|
require file.read("skills")
|
|
28
31
|
require file.read("skills/*")
|
|
29
32
|
require file.read("memory")
|
|
30
33
|
require file.read("memory/*")
|
|
34
|
+
require file.read(".lampson")
|
|
35
|
+
require file.read(".lampson/*")
|
|
31
36
|
require env("LAMPSON_*")
|
|
37
|
+
require env("OS")
|
|
38
|
+
-- computer_use solo se menciona cuando está en el catálogo (encendida + driver): vacío si no
|
|
39
|
+
let cu be computer.prompt_line()
|
|
32
40
|
let core be `You are Lampson, a coding agent that works inside the user's project through tools.
|
|
33
41
|
Operate like a careful senior engineer: precise, honest, and economical with words.
|
|
34
42
|
|
|
@@ -53,13 +61,14 @@ Operate like a careful senior engineer: precise, honest, and economical with wor
|
|
|
53
61
|
|
|
54
62
|
# Tools
|
|
55
63
|
- read (offset/limit), ls, find (glob), grep (regex): use them instead of bash with cat/ls/find/grep — line-numbered and cheaper. lsp (symbols/definition/references/hover): the file's structure without reading it, and exact navigation when grep is ambiguous — prefer lsp symbols plus a ranged read over reading a big file whole. edit: targeted replacement. write: create or fully replace.
|
|
64
|
+
- fetch (url, format, max_chars): web pages and HTTP APIs as Markdown — docs, READMEs, issues, changelogs, JSON. Use it instead of bash with curl/wget for anything you will read: it asks the site for Markdown first and strips scripts, styles and navigation, so a page costs a tenth of its HTML. A long page comes back head+tail with the full text saved to a file: read (offset/limit) or grep that file, never fetch the same URL again. Cite the URL when you use its content. Use bash+curl only for POST, custom headers or downloads.
|
|
56
65
|
- bash: shell commands from the workspace root. State does not persist between calls (cd resets) — chain with &&. Check the [exit code: N] marker on every result (it is the last line) and investigate failures before moving on. Never pipe a build or test through head/tail: the output is already truncated for you (the full text is saved to a file whose path you get) and the pipe hides the real exit code. Never run a server or watcher here.
|
|
57
66
|
- process: start/logs/stop long-running commands (dev servers, watchers). Their new log lines arrive automatically with every later tool result — do not sleep or poll. Stop what you started when the task is done.
|
|
58
67
|
- delegate: sub-agents with a fresh context (explore, plan, review, worker); several tasks in one call run in parallel, background=true returns at once and the report arrives later as a message. Give a highly detailed, self-contained brief, say whether it should write code or only research, how to verify, and exactly what to return. Its report is a self-report — verify what matters before telling the user it is done. Its output is not visible to the user: summarise it.
|
|
59
68
|
- memory: durable facts across sessions (user preferences, environment details, tool quirks, conventions). Check it before investigating. Write declarative facts, not instructions to yourself; do not store task progress or anything stale in a week. The notes live outside the project: only this tool reaches them (never ls/bash, never files inside the repo).
|
|
60
69
|
- skill: before starting a kind of task, scan the skill list below; if one matches or is even partially relevant, load it and follow it. Err on the side of loading.
|
|
61
70
|
- todo: your task list (whole-list replacement, one in_progress at a time; see Method step 1).
|
|
62
|
-
- MCP tools are named mcp_<server>_<tool>; calling one may ask the user for approval. The mcp tool lists, connects (action=add, with the user's approval) and disconnects servers; a new server's tools enter your catalog on the next turn.
|
|
71
|
+
- MCP tools are named mcp_<server>_<tool>; calling one may ask the user for approval. The mcp tool lists, connects (action=add, with the user's approval) and disconnects servers; a new server's tools enter your catalog on the next turn.{cu}
|
|
63
72
|
- Call several independent tools in one turn (reads, searches, read-only commands); serialize only when a later call depends on an earlier result. Tool outputs are truncated at 30k chars — narrow your queries.
|
|
64
73
|
|
|
65
74
|
# Tone
|