lampson 0.1.3 → 0.1.4
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 +9 -0
- package/README.md +55 -1
- package/chat.syn +123 -0
- package/lampson.ps1 +17 -3
- package/lampson.sh +17 -3
- package/lib/agents.syn +1 -1
- package/lib/approvals.syn +176 -0
- package/lib/permission.syn +18 -0
- package/lib/sched_run.syn +161 -0
- package/lib/schedule.syn +660 -0
- package/lib/session.syn +38 -1
- package/lib/settings.syn +55 -0
- package/lib/tools.syn +94 -2
- package/package.json +1 -1
- package/public/css/chat.css +56 -0
- package/public/css/layout.css +97 -0
- package/public/css/panel.css +125 -0
- package/public/css/sidebar.css +80 -0
- package/public/css/tokens.css +57 -0
- package/public/index.html +49 -1197
- package/public/js/agents.js +31 -0
- package/public/js/app.js +21 -0
- package/public/js/approvals.js +26 -0
- package/public/js/chat.js +92 -0
- package/public/js/config.js +98 -0
- package/public/js/core.js +88 -0
- package/public/js/events.js +31 -0
- package/public/js/lamps.js +112 -0
- package/public/js/lsp.js +81 -0
- package/public/js/mcp.js +74 -0
- package/public/js/memory.js +17 -0
- package/public/js/panel.js +101 -0
- package/public/js/procs.js +45 -0
- package/public/js/schedules.js +118 -0
- package/public/js/sessions.js +69 -0
- package/public/js/sidebar.js +33 -0
- package/public/js/terminal.js +49 -0
- package/public/js/theme.js +6 -0
- package/public/js/todo.js +10 -0
- package/public/js/tree.js +53 -0
- package/public/js/update.js +14 -0
- package/skills/lampson/SKILL.md +16 -0
- package/web.syn +106 -15
package/.env.example
CHANGED
|
@@ -27,3 +27,12 @@ LAMPSON_API_KEY=sk-...
|
|
|
27
27
|
|
|
28
28
|
# Reanudar una sesión en chat.syn
|
|
29
29
|
# LAMPSON_SESSION=20260826-233000
|
|
30
|
+
|
|
31
|
+
# Tareas programadas y aprobaciones a distancia (proceso residente: lampson --daemon start)
|
|
32
|
+
# LAMPSON_TZ=-03:00 # zona horaria para "daily 09:00" (default: la del sistema, `date +%z`)
|
|
33
|
+
# LAMPSON_PUBLIC_URL=https://lampson.midominio.com # URL por la que se llega a este lampson desde afuera (túnel, VPS,
|
|
34
|
+
# # edge Synsema con TLS): con esto cada aprobación pendiente trae links de un solo uso
|
|
35
|
+
# # (GET /approve/<id>/<token>?d=yes|no) para decidir desde el móvil
|
|
36
|
+
# LAMPSON_WEBHOOK_URL=https://hooks.example.com/lampson # POST JSON por cada aprobación pendiente (id, mensaje, links);
|
|
37
|
+
# # reenvialo a Telegram/Slack/mail con n8n, un bot o un .syn de 6 líneas
|
|
38
|
+
# LAMPSON_WEBHOOK_SECRET=… # firma HMAC-SHA256 del body (X-Lampson-Signature: sha256=<hex>)
|
package/README.md
CHANGED
|
@@ -29,6 +29,14 @@ language itself — and every step is visible, in the terminal or in a web UI.
|
|
|
29
29
|
under a capability ceiling, or any executable (js, py, sh…). Global in `lampson/lamps/<name>/`, per
|
|
30
30
|
project in `.lampson/lamps/<name>/` (the agent can write those). **Off by default**: you turn them on
|
|
31
31
|
from the top bar of the web UI or `/lamps on <name>`; their tools join the catalog as `lamp_<lamp>_<tool>`.
|
|
32
|
+
- **Scheduled tasks**: "every 6h", "daily 09:00", "mon,wed 08:30" — a lamp tool, a fixed shell command, or a
|
|
33
|
+
full unattended agent run from a prompt with a permission envelope fixed when you create it (`strict` /
|
|
34
|
+
`ask` / `yolo`). They run inside Lampson's resident process (`lampson --daemon start`, or the open web UI),
|
|
35
|
+
the run's session shows up as `⏰ name`, and a webhook can receive each result (for "search and send me" tasks).
|
|
36
|
+
- **Approvals from anywhere**: an unattended run that hits something dangerous waits for you — in the web UI
|
|
37
|
+
(«Aprobaciones»), and, with `LAMPSON_PUBLIC_URL` + `LAMPSON_WEBHOOK_URL`, through a signed webhook carrying
|
|
38
|
+
one-time **decision links** you can open from your phone (Telegram, mail, Slack…). Never auto-approved:
|
|
39
|
+
no answer in time = denied.
|
|
32
40
|
- **Project memory**: the agent keeps its own notes per project (`memory/<project>/*.md`, outside
|
|
33
41
|
the repo) — how to run it, gotchas, decisions — and rereads them in the next session. You can read
|
|
34
42
|
and edit them (web panel, `/memory`).
|
|
@@ -116,6 +124,46 @@ does the same with persistent sessions and memory.
|
|
|
116
124
|
> Status: developed and tested on Windows 11; the Docker image (Ubuntu 24.04) is built by CI. The Linux/macOS
|
|
117
125
|
> installer is written but not yet exercised on a real machine — issues welcome.
|
|
118
126
|
|
|
127
|
+
## Scheduled tasks and the resident process
|
|
128
|
+
|
|
129
|
+
Lampson can run things with nobody at the keyboard. Scheduled tasks run inside **any** open Lampson — the web UI
|
|
130
|
+
or the terminal REPL (a background thread ticks every 30 s). To run them with nothing open:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
lampson --daemon start # web UI + scheduler + approvals, in the background (synsema daemon)
|
|
134
|
+
lampson --daemon status # also: stop · logs · restart
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Timezone, public URL and webhook are set from the ⚙ button of the web header (tabs: General · Aprobaciones a
|
|
138
|
+
distancia · Proveedor; stored in `lampson/.lampson/config.json`, `.env` wins) or from `.env`.
|
|
139
|
+
|
|
140
|
+
Create a task from the chat («todos los días a las 9 corré los tests y avisame» — the agent calls the
|
|
141
|
+
`schedule` tool and asks you to approve the task, showing exactly what will run and with which permissions),
|
|
142
|
+
from the web sidebar («Programadas» → +), or from the terminal (`/schedule add <json>`). Three kinds:
|
|
143
|
+
|
|
144
|
+
| kind | what runs | authorization |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| `lamp` | a tool of a lamp that is ON | turning the lamp on |
|
|
147
|
+
| `bash` | one fixed command (must finish on its own) | approved once, at creation |
|
|
148
|
+
| `prompt` | a full agent turn with your instructions and a profile (`build` / `review` / `plan` / `explore`) | the permission envelope: `strict` (dangerous → denied), `ask` (dangerous → approval request, denied if unanswered within `approval_timeout`, 2 h by default), `yolo` |
|
|
149
|
+
|
|
150
|
+
Schedules: `every 6h` · `every 30m` · `daily 09:00` · `mon,wed 08:30` · `weekdays 09:00` (local time; `LAMPSON_TZ`
|
|
151
|
+
overrides). Synsema's `cron_every` is a pure interval, so a 30 s tick in `web.syn` translates it to wall-clock time;
|
|
152
|
+
a run missed while the daemon was down executes when it comes back and is marked as late. State lives in
|
|
153
|
+
`.lampson/schedules.json`, logs in `.lampson/schedules/<id>.log`; a `prompt` run also leaves a normal session (`⏰ name`)
|
|
154
|
+
with its trace. `notify` = a webhook URL that receives each result as JSON.
|
|
155
|
+
|
|
156
|
+
**Approving from your phone.** Set `LAMPSON_PUBLIC_URL` (how this Lampson is reached from outside — a tunnel, a VPS,
|
|
157
|
+
or a Synsema edge with TLS in front) and `LAMPSON_WEBHOOK_URL` (+ `LAMPSON_WEBHOOK_SECRET`, HMAC-SHA256 in
|
|
158
|
+
`X-Lampson-Signature`). Every pending approval — from a scheduled run or from the chat — POSTs `{id, message, why,
|
|
159
|
+
expires_at, respond_link_yes, respond_link_no}`; forward the links wherever you read (n8n, a bot, a 6-line `.syn`).
|
|
160
|
+
`GET /approve/<id>/<token>?d=yes|no` is public on purpose: the 32-byte one-time token is the authorization, and it
|
|
161
|
+
dies with the deadline. `GET /api/approvals` (loopback) lists what is pending, never the tokens.
|
|
162
|
+
|
|
163
|
+
For a real server use systemd (`synsema serve web.syn`, `Restart=always`) instead of `synsema daemon`, which has no
|
|
164
|
+
boot start / crash restart. One box, several projects: one `web.syn` per project on its own port, behind a Synsema
|
|
165
|
+
edge that terminates TLS and routes by host (see the Synsema deploy docs).
|
|
166
|
+
|
|
119
167
|
## Use
|
|
120
168
|
|
|
121
169
|
```bash
|
|
@@ -185,7 +233,13 @@ run Lampson in a container.
|
|
|
185
233
|
lampson.ps1 / .sh launcher: mounts ./workspace, starts terminal or web
|
|
186
234
|
chat.syn terminal REPL (colors, approvals via Synsema's native `approve`)
|
|
187
235
|
web.syn HTTP server: POST /api/chat → SSE events; sessions, tree, file viewer, processes, ports
|
|
188
|
-
public/
|
|
236
|
+
public/ web UI (no build step, no dependencies; classic scripts served by `static`)
|
|
237
|
+
index.html markup only: header, the two side panels, the chat; loads css/ and js/ in order
|
|
238
|
+
css/ tokens (fonts, palette, base) · layout (grid, header, panels, chat, composer) · sidebar · chat · panel
|
|
239
|
+
js/core.js shared state + helpers ($, esc, md, add, api, showPane/showText, inlineConfirm, debounce, empty)
|
|
240
|
+
js/panel.js THE modal component: one shell, three layouts (browse = search + list + detail, tabs, form)
|
|
241
|
+
js/<view>.js one file per thing on screen: sessions, chat, tree, terminal, procs, agents, memory, todo,
|
|
242
|
+
mcp, lsp, lamps, schedules, approvals, config (+ provider), update, events (SSE), app (boot)
|
|
189
243
|
lib/
|
|
190
244
|
provider.syn config from .env · chat(cfg, messages, catalog) · retry with backoff
|
|
191
245
|
loop.syn run_turn(): LLM → tool calls → permissions → call_tool → results → repeat; doom-loop guard; compaction
|
package/chat.syn
CHANGED
|
@@ -14,6 +14,7 @@ require net
|
|
|
14
14
|
require time
|
|
15
15
|
require stdin
|
|
16
16
|
require exec
|
|
17
|
+
require random
|
|
17
18
|
require env("LAMPSON_*")
|
|
18
19
|
require env("OS")
|
|
19
20
|
require secret("LAMPSON_*")
|
|
@@ -51,6 +52,9 @@ use "./lib/mcp.syn" as mcp
|
|
|
51
52
|
use "./lib/lamps.syn" as lamps
|
|
52
53
|
use "./lib/lsp.syn" as lsp
|
|
53
54
|
use "./lib/tools/todo.syn" as todo
|
|
55
|
+
use "./lib/schedule.syn" as schedule
|
|
56
|
+
use "./lib/sched_run.syn" as sched_run
|
|
57
|
+
use "./lib/approvals.syn" as approvals
|
|
54
58
|
|
|
55
59
|
-- ---------- UI de terminal ----------
|
|
56
60
|
-- Colores ANSI (Windows Terminal, PowerShell 7, cualquier terminal moderna). LAMPSON_NO_COLOR=1 los apaga.
|
|
@@ -344,6 +348,8 @@ let COMMANDS be [
|
|
|
344
348
|
["/mcp", "[add <nombre> <comando…> [--project] | remove <nombre>]", "servers MCP: listar, conectar o quitar (global: lampson/.lampson/mcp.json · proyecto: .lampson/mcp.json)"],
|
|
345
349
|
["/lamps", "[on <nombre> | off <nombre> | run <lámpara> <tool> [json] | remove <nombre>]", "lámparas (plugins de tools): listar, encender o apagar (global: lampson/lamps/ · proyecto: .lampson/lamps/)"],
|
|
346
350
|
["/lsp", "[add <typescript|python|rust|go|css|html> [--project] | add <nombre> <comando…> --ext .x=lang | remove <nombre>]", "language servers (navegación semántica: symbols/definition/references/hover); arrancan en la primera consulta"],
|
|
351
|
+
["/schedule", "[add <json> | run <id> | on <id> | off <id> | remove <id> | log <id>]", "tareas programadas (cada 6h, todos los días a las 9…): lámpara, comando o corrida del agente; corren mientras lampson esté abierto (o con lampson --daemon start, sin nada abierto)"],
|
|
352
|
+
["/approve", "<id> yes|no", "responder una aprobación pendiente de una tarea programada que corre en background"],
|
|
347
353
|
["/out", "[n]", "resultado completo de la última tool del turno (n = contar hacia atrás: /out 2 es la anteúltima)"],
|
|
348
354
|
["/verbose", "", "alternar: mostrar SIEMPRE el output completo de cada tool (queda guardado en config.json)"],
|
|
349
355
|
["/trace", "[n]", "traza legible de esta sesión (pasos, tools, tiempos, tokens, errores): .lampson/trace/<sesión>.log"],
|
|
@@ -403,6 +409,16 @@ task complete_args(cmd, head, last)
|
|
|
403
409
|
when trim(head) == "add"
|
|
404
410
|
give sort_by(keys(lsp.PRESETS), (x) => x)
|
|
405
411
|
give ["--project", "--ext"]
|
|
412
|
+
when cmd == "/approve"
|
|
413
|
+
when first
|
|
414
|
+
give apply((a) => a["id"], approvals.pending())
|
|
415
|
+
give ["yes", "no"]
|
|
416
|
+
when cmd == "/schedule"
|
|
417
|
+
when first
|
|
418
|
+
give ["add", "run", "on", "off", "remove", "log"]
|
|
419
|
+
when starts_with(trim(head), "add")
|
|
420
|
+
give []
|
|
421
|
+
give apply((s) => s["id"], schedule.summary())
|
|
406
422
|
when cmd == "/agent"
|
|
407
423
|
give ["build", "plan", "review", "explore", "worker"]
|
|
408
424
|
when cmd == "/model"
|
|
@@ -672,6 +688,47 @@ task opts_for(p, mode)
|
|
|
672
688
|
task lampson_subagent(spec_json)
|
|
673
689
|
give agents.run_child_json(spec_json)
|
|
674
690
|
|
|
691
|
+
-- Tareas programadas TAMBIÉN en la terminal: `cron_every` no corre mientras el hilo principal está en el teclado,
|
|
692
|
+
-- pero un `agent` sí (hilo real, como el Spinner). Cada 30 s llama al tick (heartbeat + corridas vencidas); una
|
|
693
|
+
-- corrida `prompt` transcurre al lado del chat sin tocar su sesión (blackboard lampson:sched:session) y si pide
|
|
694
|
+
-- permiso lo dice por consola: /approve <id> yes|no (o link/webhook si están configurados).
|
|
695
|
+
task lampson_sched_tick()
|
|
696
|
+
give sched_run.tick()
|
|
697
|
+
|
|
698
|
+
agent Sched
|
|
699
|
+
require net
|
|
700
|
+
require time
|
|
701
|
+
require exec
|
|
702
|
+
require random
|
|
703
|
+
require env("LAMPSON_*")
|
|
704
|
+
require env("OS")
|
|
705
|
+
require secret("LAMPSON_*")
|
|
706
|
+
require file("workspace")
|
|
707
|
+
require file("workspace/*")
|
|
708
|
+
require file.read("skills")
|
|
709
|
+
require file.read("skills/*")
|
|
710
|
+
require file.read("lamps")
|
|
711
|
+
require file.read("lamps/*")
|
|
712
|
+
require file("memory")
|
|
713
|
+
require file("memory/*")
|
|
714
|
+
require file(".lampson")
|
|
715
|
+
require file(".lampson/*")
|
|
716
|
+
let alive be true
|
|
717
|
+
let waited be 25
|
|
718
|
+
while alive
|
|
719
|
+
observe "lampson:spinner:stop" as st
|
|
720
|
+
when st == true
|
|
721
|
+
set alive to false
|
|
722
|
+
otherwise when waited >= 30
|
|
723
|
+
set waited to 0
|
|
724
|
+
try
|
|
725
|
+
lampson_sched_tick()
|
|
726
|
+
recover err
|
|
727
|
+
print("[lampson] scheduler: " + text(err))
|
|
728
|
+
otherwise
|
|
729
|
+
sleep(1)
|
|
730
|
+
set waited to waited + 1
|
|
731
|
+
|
|
675
732
|
-- !comando del usuario en un pseudo-terminal: la salida se ve EN VIVO y, si el proceso se queda esperando
|
|
676
733
|
-- (prompt y/N, contraseña, REPL, `npm init`…), lo que escribas se le manda como teclas + Enter.
|
|
677
734
|
-- Enter vacío = seguir esperando · ^C = cortar. Devuelve la salida (sin ANSI) para el contexto del agente.
|
|
@@ -765,6 +822,7 @@ banner(env_info["cwd"], cfg, profile, opts["permission_mode"], sid)
|
|
|
765
822
|
share nothing as "lampson:busy"
|
|
766
823
|
share false as "lampson:spinner:stop"
|
|
767
824
|
spawn Spinner with esc = ESC, color = COLOR
|
|
825
|
+
spawn Sched
|
|
768
826
|
flush()
|
|
769
827
|
|
|
770
828
|
let total_usage be {"input": 0, "output": 0}
|
|
@@ -1005,6 +1063,71 @@ while running
|
|
|
1005
1063
|
each s in ss
|
|
1006
1064
|
print(" " + (when s["status"] == "ready" then green("● ") otherwise (when s["status"] == "error" or s["status"] == "exited" then red("○ ") otherwise dim("○ "))) + pad(s["name"], 12) + dim(pad(s["scope"], 8) + pad(s["status"], 8) + join(s["extensions"], " ") + " $ " + s["command"]) + (when s["error"] != nothing then red(" " + text(s["error"])) otherwise ""))
|
|
1007
1065
|
print(" " + dim("idle = arranca en la primera consulta del agente"))
|
|
1066
|
+
otherwise when input == "/schedule" or starts_with(input, "/schedule ")
|
|
1067
|
+
-- tareas programadas (lib/schedule.syn). Crear: JSON, o pedírselo al agente ("todos los días a las 9 corré…").
|
|
1068
|
+
-- Las corre el daemon (web.syn con `lampson --daemon start`); `/schedule run` las corre acá mismo, con
|
|
1069
|
+
-- el menú de aprobación de la terminal si la tarea es `ask`.
|
|
1070
|
+
let srest be trim(slice(input, 9, length(input)))
|
|
1071
|
+
let stoks be where(split(srest, " "), (x) => x != "")
|
|
1072
|
+
let sact be when length(stoks) > 0 then stoks[0] otherwise ""
|
|
1073
|
+
let sid_arg be when length(stoks) > 1 then stoks[1] otherwise ""
|
|
1074
|
+
when sact == "add"
|
|
1075
|
+
let sjson be trim(slice(srest, 3, length(srest)))
|
|
1076
|
+
when sjson == ""
|
|
1077
|
+
print(" uso: /schedule add {\"name\": \"…\", \"when\": \"daily 09:00\", \"action\": {\"type\": \"bash\", \"command\": \"npm test\"}, \"permission\": \"ask\"}")
|
|
1078
|
+
print(" " + dim("when: every 6h · daily 09:00 · mon,wed 08:30 · weekdays 09:00 action.type: lamp {lamp, tool, args} · bash {command} · prompt {prompt, agent}"))
|
|
1079
|
+
print(" " + dim("o pedíselo al agente en lenguaje natural: «todos los días a las 9 revisá los tests y avisame»"))
|
|
1080
|
+
otherwise
|
|
1081
|
+
try
|
|
1082
|
+
let st be schedule.add(json_decode(sjson))
|
|
1083
|
+
print(" " + green("⏰ programada «" + st["name"] + "» (" + st["id"] + "): " + schedule.describe_plan(st["plan"]) + " · próxima " + schedule.fmt_local(st["next_run"])))
|
|
1084
|
+
recover err
|
|
1085
|
+
print(" " + red(text(err)))
|
|
1086
|
+
otherwise when sact == "run" and sid_arg != ""
|
|
1087
|
+
let st be schedule.get(sid_arg)
|
|
1088
|
+
when st == nothing
|
|
1089
|
+
print(" " + red("no existe la tarea " + sid_arg))
|
|
1090
|
+
otherwise
|
|
1091
|
+
print(" " + yellow("⏰ corriendo «" + st["name"] + "» · " + schedule.describe_action(st["action"])))
|
|
1092
|
+
flush()
|
|
1093
|
+
let sr be sched_run.run(st, ask_user, on_event)
|
|
1094
|
+
print(" " + (when sr["last_status"] == "ok" then green("✓ ") otherwise red("✗ ")) + sr["last_status"] + " · " + first_line(sr["last_summary"], 160))
|
|
1095
|
+
print(" " + dim("log: " + schedule.LOG_DIR + "/" + st["id"] + ".log"))
|
|
1096
|
+
otherwise when (sact == "on" or sact == "off") and sid_arg != ""
|
|
1097
|
+
try
|
|
1098
|
+
print(" " + schedule.set_enabled(sid_arg, sact == "on"))
|
|
1099
|
+
recover err
|
|
1100
|
+
print(" " + red(text(err)))
|
|
1101
|
+
otherwise when sact == "remove" and sid_arg != ""
|
|
1102
|
+
try
|
|
1103
|
+
print(" " + schedule.remove(sid_arg))
|
|
1104
|
+
recover err
|
|
1105
|
+
print(" " + red(text(err)))
|
|
1106
|
+
otherwise when sact == "log" and sid_arg != ""
|
|
1107
|
+
let tl be schedule.log_tail(sid_arg, 60)
|
|
1108
|
+
print(when tl == "" then " sin corridas todavía" otherwise tl)
|
|
1109
|
+
otherwise
|
|
1110
|
+
let ss be schedule.summary()
|
|
1111
|
+
let age be schedule.daemon_age()
|
|
1112
|
+
when length(ss) == 0
|
|
1113
|
+
print(" sin tareas programadas. Pedísela al agente («cada 6 horas corré los tests y avisame») o: " + cyan("/schedule add <json>"))
|
|
1114
|
+
each s in ss
|
|
1115
|
+
print(" " + (when s["running"] then yellow("● ") otherwise (when s["enabled"] then green("● ") otherwise dim("○ "))) + pad(s["id"], 22) + dim(pad(s["plan"], 30)) + s["action_text"])
|
|
1116
|
+
print(" " + dim("permisos " + s["permission"] + " · próxima " + s["next_local"] + (when s["last_run"] != nothing then " · última " + s["last_status"] + ": " + first_line(s["last_summary"], 80) otherwise "")))
|
|
1117
|
+
let pend be approvals.pending()
|
|
1118
|
+
each ap in pend
|
|
1119
|
+
print(" " + yellow("⚠ aprobación pendiente " + ap["id"] + ": " + first_line(ap["message"], 100)) + dim(" → /approve " + ap["id"] + " yes|no"))
|
|
1120
|
+
print(" " + dim("corren solas mientras lampson esté abierto (tick cada " + text(schedule.TICK_SECONDS) + " s) · con todo cerrado: lampson --daemon start · run/on/off/remove/log <id>"))
|
|
1121
|
+
otherwise when starts_with(input, "/approve ")
|
|
1122
|
+
let atoks be where(split(trim(slice(input, 9, length(input))), " "), (x) => x != "")
|
|
1123
|
+
when length(atoks) < 2 or not contains(["yes", "no", "si", "sí", "y", "n"], lower(atoks[1]))
|
|
1124
|
+
print(" uso: /approve <id> yes|no (pendientes: /schedule)")
|
|
1125
|
+
otherwise
|
|
1126
|
+
let yes be contains(["yes", "si", "sí", "y"], lower(atoks[1]))
|
|
1127
|
+
when approvals.answer(atoks[0], yes)
|
|
1128
|
+
print(" " + (when yes then green("✓ permitido ") otherwise red("✗ denegado ")) + atoks[0])
|
|
1129
|
+
otherwise
|
|
1130
|
+
print(" " + red("no hay una aprobación pendiente con id " + atoks[0]))
|
|
1008
1131
|
otherwise when input == "/lamps" or starts_with(input, "/lamps ")
|
|
1009
1132
|
let lrest be trim(slice(input, 6, length(input)))
|
|
1010
1133
|
when starts_with(lrest, "run ")
|
package/lampson.ps1
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
# lampson --workspace C:\otro\proyecto # elegir la ubicación explícitamente (también -Workspace)
|
|
6
6
|
# lampson --agent plan # perfil inicial: build | plan | review | explore
|
|
7
7
|
# lampson --yolo | --strict | --ask # permisos para comandos peligrosos (--dangerously-skip-permissions = --yolo)
|
|
8
|
+
# lampson --daemon start|stop|status|logs|restart # proceso residente (web + tareas programadas + aprobaciones)
|
|
8
9
|
# lampson --update # actualizar Lampson (git pull) y salir
|
|
9
10
|
# lampson --help
|
|
10
11
|
#
|
|
@@ -17,12 +18,13 @@ $caller = (Get-Location).Path
|
|
|
17
18
|
$mount = Join-Path $here "workspace"
|
|
18
19
|
|
|
19
20
|
# --- args: acepta --flag y -Flag, sin distinguir mayúsculas ---
|
|
20
|
-
$Workspace = ""; $Web = $false; $Agent = ""; $Perm = ""
|
|
21
|
+
$Workspace = ""; $Web = $false; $Agent = ""; $Perm = ""; $Daemon = ""
|
|
21
22
|
$i = 0
|
|
22
23
|
while ($i -lt $args.Count) {
|
|
23
24
|
$a = [string]$args[$i]
|
|
24
25
|
switch -Regex ($a.ToLower()) {
|
|
25
26
|
'^--?(web|w)$' { $Web = $true }
|
|
27
|
+
'^--?(daemon|d)$' { $i++; $Daemon = if ($i -lt $args.Count) { ([string]$args[$i]).ToLower() } else { "status" } }
|
|
26
28
|
'^--?(workspace|ws)$' { $i++; $Workspace = [string]$args[$i] }
|
|
27
29
|
'^--?(agent|a)$' { $i++; $Agent = [string]$args[$i] }
|
|
28
30
|
'^--?(yolo|y|dangerously-skip-permissions)$' { $Perm = "yolo" }
|
|
@@ -30,7 +32,7 @@ while ($i -lt $args.Count) {
|
|
|
30
32
|
'^--?(ask)$' { $Perm = "ask" }
|
|
31
33
|
'^--?(permission|p)$' { $i++; $Perm = ([string]$args[$i]).ToLower() }
|
|
32
34
|
'^--?(update|u)$' { Write-Host "actualizando Lampson en $here"; git -C $here pull --ff-only origin main; Write-Host ("lampson " + (git -C $here rev-parse --short HEAD)); exit $LASTEXITCODE }
|
|
33
|
-
'^--?(help|h|\?)$' { Get-Content $PSCommandPath | Select-Object -Skip 1 -First
|
|
35
|
+
'^--?(help|h|\?)$' { Get-Content $PSCommandPath | Select-Object -Skip 1 -First 10 | ForEach-Object { $_.TrimStart('#',' ') }; exit 0 }
|
|
34
36
|
default { if ($Workspace -eq "" -and -not $a.StartsWith("-")) { $Workspace = $a } else { Write-Error "argumento desconocido: $a (probá lampson --help)"; exit 1 } }
|
|
35
37
|
}
|
|
36
38
|
$i++
|
|
@@ -49,6 +51,12 @@ if ($Workspace -ne "") {
|
|
|
49
51
|
if (Test-Path -LiteralPath $mount) {
|
|
50
52
|
$item = Get-Item -LiteralPath $mount -Force
|
|
51
53
|
if ($item.LinkType -ne "Junction") { Write-Error "./workspace existe y no es una junction; movelo antes de montar otro proyecto"; exit 1 }
|
|
54
|
+
# un daemon/web corriendo sobre OTRO proyecto (latido reciente del scheduler) quedaría apuntando a este: aviso
|
|
55
|
+
$prev = $item.Target; if ($prev -is [array]) { $prev = $prev[0] }
|
|
56
|
+
$hb = Join-Path $here ".lampson\schedules.heartbeat"
|
|
57
|
+
if ($prev -and $prev.TrimEnd('\').ToLower() -ne $target.TrimEnd('\').ToLower() -and (Test-Path -LiteralPath $hb) -and ((Get-Date) - (Get-Item -LiteralPath $hb).LastWriteTime).TotalSeconds -lt 90) {
|
|
58
|
+
Write-Host "AVISO: hay un lampson (web/daemon) corriendo sobre $prev; al montar $target sus tareas programadas quedan en pausa. Paralo o reinicialo desde su carpeta: lampson --daemon restart" -ForegroundColor Yellow
|
|
59
|
+
}
|
|
52
60
|
$item.Delete()
|
|
53
61
|
}
|
|
54
62
|
New-Item -ItemType Junction -Path $mount -Target $target | Out-Null
|
|
@@ -76,7 +84,13 @@ try {
|
|
|
76
84
|
$env:LAMPSON_WORKSPACE = $target
|
|
77
85
|
if ($Agent -ne "") { $env:LAMPSON_AGENT = $Agent }
|
|
78
86
|
if ($Perm -ne "") { $env:LAMPSON_PERMISSION = $Perm }
|
|
79
|
-
if ($
|
|
87
|
+
if ($Daemon -ne "") {
|
|
88
|
+
# proceso residente: web.syn en background (synsema daemon, sin config del sistema; para producción,
|
|
89
|
+
# systemd/NSSM con `synsema serve web.syn`). Ejecuta las tareas programadas y atiende aprobaciones por link.
|
|
90
|
+
if ($Daemon -notmatch '^(start|stop|status|logs|restart)$') { Write-Error "uso: lampson --daemon start|stop|status|logs|restart"; exit 1 }
|
|
91
|
+
if ($Daemon -eq "start" -or $Daemon -eq "restart") { Write-Host "Lampson daemon · workspace: $target · http://127.0.0.1:8080" }
|
|
92
|
+
synsema daemon $Daemon web.syn
|
|
93
|
+
} elseif ($Web) {
|
|
80
94
|
Write-Host "Lampson web · workspace: $target"
|
|
81
95
|
Write-Host "abrí http://127.0.0.1:8080 (Ctrl+C para parar)"
|
|
82
96
|
synsema serve web.syn
|
package/lampson.sh
CHANGED
|
@@ -3,20 +3,22 @@
|
|
|
3
3
|
# cd /mi/proyecto && lampson # REPL (con lampson/ en el PATH)
|
|
4
4
|
# cd /mi/proyecto && lampson --web # servidor web en http://127.0.0.1:8080
|
|
5
5
|
# lampson --workspace /otro/proyecto [--web] [--agent plan]
|
|
6
|
+
# lampson --daemon start|stop|status|logs|restart # proceso residente (web + tareas programadas + aprobaciones)
|
|
6
7
|
set -euo pipefail
|
|
7
8
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
9
|
caller="$(pwd)"
|
|
9
|
-
ws=""; web=0
|
|
10
|
+
ws=""; web=0; daemon=""
|
|
10
11
|
while [ $# -gt 0 ]; do
|
|
11
12
|
case "$1" in
|
|
12
13
|
--web) web=1 ;;
|
|
14
|
+
--daemon) shift; daemon="${1:-status}" ;;
|
|
13
15
|
--workspace) shift; ws="$1" ;;
|
|
14
16
|
--agent) shift; export LAMPSON_AGENT="$1" ;;
|
|
15
17
|
--yolo|--dangerously-skip-permissions) export LAMPSON_PERMISSION=yolo ;;
|
|
16
18
|
--strict) export LAMPSON_PERMISSION=strict ;;
|
|
17
19
|
--ask) export LAMPSON_PERMISSION=ask ;;
|
|
18
20
|
--update) echo "actualizando Lampson en $here"; git -C "$here" pull --ff-only origin main; echo "lampson $(git -C "$here" rev-parse --short HEAD)"; exit $? ;;
|
|
19
|
-
*) echo "uso: lampson [--web] [--workspace RUTA] [--agent PERFIL] [--yolo|--strict|--ask] [--update]" >&2; exit 1 ;;
|
|
21
|
+
*) echo "uso: lampson [--web] [--daemon start|stop|status|logs|restart] [--workspace RUTA] [--agent PERFIL] [--yolo|--strict|--ask] [--update]" >&2; exit 1 ;;
|
|
20
22
|
esac
|
|
21
23
|
shift
|
|
22
24
|
done
|
|
@@ -25,6 +27,13 @@ if [ -n "$ws" ]; then
|
|
|
25
27
|
[ -d "$ws" ] || { echo "el workspace no existe o no es un directorio: $ws" >&2; exit 1; }
|
|
26
28
|
ws="$(cd "$ws" && pwd)"
|
|
27
29
|
if [ -e "$here/workspace" ] && [ ! -L "$here/workspace" ]; then echo "./workspace existe y no es un symlink" >&2; exit 1; fi
|
|
30
|
+
# un daemon/web corriendo sobre OTRO proyecto (latido reciente del scheduler) quedaría apuntando a este: aviso
|
|
31
|
+
if [ -L "$here/workspace" ] && [ -f "$here/.lampson/schedules.heartbeat" ]; then
|
|
32
|
+
prev="$(readlink -f "$here/workspace")"
|
|
33
|
+
if [ "$prev" != "$ws" ] && [ $(( $(date +%s) - $(stat -c %Y "$here/.lampson/schedules.heartbeat") )) -lt 90 ]; then
|
|
34
|
+
echo "AVISO: hay un lampson (web/daemon) corriendo sobre $prev; al montar $ws sus tareas programadas quedan en pausa. Paralo o reinicialo desde su carpeta: lampson --daemon restart" >&2
|
|
35
|
+
fi
|
|
36
|
+
fi
|
|
28
37
|
rm -f "$here/workspace"; ln -s "$ws" "$here/workspace"
|
|
29
38
|
elif [ ! -e "$here/workspace" ]; then
|
|
30
39
|
echo "no hay workspace: corré lampson desde el directorio del proyecto, o --workspace /ruta" >&2; exit 1
|
|
@@ -39,4 +48,9 @@ for pair in "skills-global:$HOME/.agents/skills" "skills-claude:$HOME/.claude/sk
|
|
|
39
48
|
done
|
|
40
49
|
echo "workspace -> $LAMPSON_WORKSPACE"
|
|
41
50
|
cd "$here"
|
|
42
|
-
if [ "$
|
|
51
|
+
if [ -n "$daemon" ]; then
|
|
52
|
+
# proceso residente en background (synsema daemon: sin arranque al boot ni reinicio; para producción, systemd
|
|
53
|
+
# con `synsema serve web.syn`). Ejecuta las tareas programadas y atiende aprobaciones por link.
|
|
54
|
+
case "$daemon" in start|stop|status|logs|restart) ;; *) echo "uso: lampson --daemon start|stop|status|logs|restart" >&2; exit 1 ;; esac
|
|
55
|
+
exec synsema daemon "$daemon" web.syn
|
|
56
|
+
elif [ "$web" = 1 ]; then echo "web: http://127.0.0.1:8080"; exec synsema serve web.syn; else exec synsema run chat.syn; fi
|
package/lib/agents.syn
CHANGED
|
@@ -40,7 +40,7 @@ let PARALLEL be 4
|
|
|
40
40
|
|
|
41
41
|
export let PROFILES be {
|
|
42
42
|
"build": {
|
|
43
|
-
"tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "bash", "process", "memory", "todo", "skill", "mcp", "lamp", "delegate"],
|
|
43
|
+
"tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "bash", "process", "memory", "todo", "skill", "mcp", "lamp", "schedule", "delegate"],
|
|
44
44
|
"steps": 40,
|
|
45
45
|
"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
46
|
},
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
-- lib/approvals.syn — aprobaciones humanas fuera de la terminal: cola compartida + webhook + links de decisión
|
|
2
|
+
--
|
|
3
|
+
-- Un solo mecanismo para las puertas por las que un humano aprueba algo que el agente quiere hacer:
|
|
4
|
+
-- 1. la UI web (POST /api/approve desde el chat, o desde el panel «Aprobaciones» para las tareas programadas);
|
|
5
|
+
-- 2. la terminal: /approve <id> yes|no (una tarea programada corriendo en background mientras chateás);
|
|
6
|
+
-- 3. un LINK firmado que llega por el canal que sea (Telegram, mail, Slack…): GET /approve/<id>/<token>?d=yes|no
|
|
7
|
+
-- — de un solo uso; el token es un secreto por aprobación (32 bytes CSPRNG), igual que el `approve` nativo
|
|
8
|
+
-- de Synsema bajo serve (doc 62-human), solo que acá también sirve para las tareas del cron y para la UI;
|
|
9
|
+
-- 4. la consola del proceso (el link se imprime ahí también).
|
|
10
|
+
-- Además, si LAMPSON_WEBHOOK_URL está configurado (o config.json {"webhook_url"}), cada aprobación pendiente dispara
|
|
11
|
+
-- un POST JSON con {id, message, why, expires_at, respond_link_yes, respond_link_no} firmado con HMAC-SHA256
|
|
12
|
+
-- (X-Lampson-Signature: sha256=<hex>, secreto LAMPSON_WEBHOOK_SECRET) — el mismo patrón que Stripe/GitHub, así
|
|
13
|
+
-- cualquier receptor (n8n, un bot, otro .syn) reenvía los links. Los links necesitan LAMPSON_PUBLIC_URL (la URL
|
|
14
|
+
-- por la que se llega a este lampson desde afuera: un túnel, el VPS, un edge Synsema con TLS).
|
|
15
|
+
-- Fire-and-forget (10 s, un intento): un canal caído nunca bloquea; la UI y la consola quedan como respaldo.
|
|
16
|
+
--
|
|
17
|
+
-- Estado: BLACKBOARD (share/observe), que existe bajo `serve` y bajo `run` y lo ven handlers, agentes y ticks
|
|
18
|
+
-- (`state_*` solo existe bajo serve — verificado 2026-08-29). Claves: "approval:meta:<id>" (visible, sin token),
|
|
19
|
+
-- "approval:token:<id>", "approval:answer:<id>" (yes|no) y el índice "approval:index" (lista de ids).
|
|
20
|
+
--
|
|
21
|
+
-- Garantía: NUNCA se auto-aprueba. Sin respuesta al vencer el plazo → denegado (fail-closed), como Synsema.
|
|
22
|
+
|
|
23
|
+
use "./settings.syn" as settings
|
|
24
|
+
|
|
25
|
+
task read_cfg(key, envname)
|
|
26
|
+
let v be env(envname, "")
|
|
27
|
+
when v != ""
|
|
28
|
+
give v
|
|
29
|
+
let doc be settings.load()
|
|
30
|
+
when contains(doc, key)
|
|
31
|
+
give text(doc[key])
|
|
32
|
+
give ""
|
|
33
|
+
|
|
34
|
+
export task webhook_url()
|
|
35
|
+
require env("LAMPSON_*")
|
|
36
|
+
require file.read(".lampson")
|
|
37
|
+
require file.read(".lampson/*")
|
|
38
|
+
give read_cfg("webhook_url", "LAMPSON_WEBHOOK_URL")
|
|
39
|
+
|
|
40
|
+
export task public_url()
|
|
41
|
+
require env("LAMPSON_*")
|
|
42
|
+
require file.read(".lampson")
|
|
43
|
+
require file.read(".lampson/*")
|
|
44
|
+
let u be read_cfg("public_url", "LAMPSON_PUBLIC_URL")
|
|
45
|
+
while length(u) > 0 and slice(u, length(u) - 1, length(u)) == "/"
|
|
46
|
+
set u to slice(u, 0, length(u) - 1)
|
|
47
|
+
give u
|
|
48
|
+
|
|
49
|
+
task secret_text()
|
|
50
|
+
give read_cfg("webhook_secret", "LAMPSON_WEBHOOK_SECRET")
|
|
51
|
+
|
|
52
|
+
task index()
|
|
53
|
+
observe "approval:index" as ix
|
|
54
|
+
give when ix == nothing then [] otherwise ix
|
|
55
|
+
|
|
56
|
+
task meta_of(id)
|
|
57
|
+
observe "approval:meta:" + id as m
|
|
58
|
+
give m
|
|
59
|
+
|
|
60
|
+
task set_meta(id, m)
|
|
61
|
+
share m as "approval:meta:" + id
|
|
62
|
+
|
|
63
|
+
-- abre una aprobación: id único, token de un solo uso, metadatos visibles (sin token) para la UI.
|
|
64
|
+
-- source = "chat" | "schedule:<id>"; message = lo que se quiere hacer; why = por qué pide permiso.
|
|
65
|
+
export task open(source, message, why, timeout_s)
|
|
66
|
+
require time
|
|
67
|
+
require random
|
|
68
|
+
require net
|
|
69
|
+
require env("LAMPSON_*")
|
|
70
|
+
require file.read(".lampson")
|
|
71
|
+
require file.read(".lampson/*")
|
|
72
|
+
let id be "a" + slice(text(floor(now() * 1000)), 4, 13) + "-" + slice(token(16), 0, 6)
|
|
73
|
+
let tok be token(32)
|
|
74
|
+
let meta be {"id": id, "source": source, "message": message, "why": why, "created": now(), "expires_at": floor(now() + timeout_s), "status": "pending"}
|
|
75
|
+
set_meta(id, meta)
|
|
76
|
+
share tok as "approval:token:" + id
|
|
77
|
+
share nothing as "approval:answer:" + id
|
|
78
|
+
share append(index(), id) as "approval:index"
|
|
79
|
+
bus_publish("approval.request", meta)
|
|
80
|
+
let links be links_for(id, tok)
|
|
81
|
+
print("[lampson] aprobación pendiente " + id + " — " + message + " (vence en " + text(floor(timeout_s)) + " s)" + (when links["yes"] != "" then "\n sí: " + links["yes"] + "\n no: " + links["no"] otherwise "\n respondé desde la UI web (Aprobaciones), con /approve " + id + " yes|no en la terminal, o configurá la URL pública para recibir links"))
|
|
82
|
+
notify(meta, links)
|
|
83
|
+
give id
|
|
84
|
+
|
|
85
|
+
task links_for(id, tok)
|
|
86
|
+
let base be public_url()
|
|
87
|
+
when base == ""
|
|
88
|
+
give {"yes": "", "no": "", "path": "/approve/" + id + "/" + tok}
|
|
89
|
+
give {"yes": base + "/approve/" + id + "/" + tok + "?d=yes", "no": base + "/approve/" + id + "/" + tok + "?d=no", "path": "/approve/" + id + "/" + tok}
|
|
90
|
+
|
|
91
|
+
-- webhook firmado (fire-and-forget)
|
|
92
|
+
task notify(meta, links)
|
|
93
|
+
let url be webhook_url()
|
|
94
|
+
when url == ""
|
|
95
|
+
give false
|
|
96
|
+
let payload be {"id": meta["id"], "type": "approve", "source": meta["source"], "message": meta["message"], "why": meta["why"], "expires_at": meta["expires_at"], "respond_path": links["path"], "respond_link_yes": links["yes"], "respond_link_no": links["no"]}
|
|
97
|
+
let body be json_encode(payload)
|
|
98
|
+
let headers be {"Content-Type": "application/json", "User-Agent": "lampson"}
|
|
99
|
+
let sec be secret_text()
|
|
100
|
+
when sec != ""
|
|
101
|
+
set headers["X-Lampson-Signature"] to "sha256=" + hmac_sha256(body, sec)
|
|
102
|
+
try
|
|
103
|
+
let r be http_post(url, body, headers)
|
|
104
|
+
when not r["ok"]
|
|
105
|
+
print("[lampson] webhook " + url + " respondió " + text(r["status"]))
|
|
106
|
+
give r["ok"]
|
|
107
|
+
recover err
|
|
108
|
+
print("[lampson] webhook falló: " + text(err))
|
|
109
|
+
give false
|
|
110
|
+
|
|
111
|
+
-- espera (polling) hasta que alguien responda o venza el plazo. Devuelve true/false; sin respuesta = false.
|
|
112
|
+
export task wait(id, timeout_s)
|
|
113
|
+
require time
|
|
114
|
+
let deadline be now() + timeout_s
|
|
115
|
+
let answer be nothing
|
|
116
|
+
while answer == nothing and now() < deadline
|
|
117
|
+
observe "approval:answer:" + id as a
|
|
118
|
+
set answer to a
|
|
119
|
+
when answer == nothing
|
|
120
|
+
sleep(0.5)
|
|
121
|
+
let meta be meta_of(id)
|
|
122
|
+
let ok be answer == "yes"
|
|
123
|
+
when meta != nothing
|
|
124
|
+
set meta["status"] to when answer == nothing then "expired" otherwise (when ok then "approved" otherwise "denied")
|
|
125
|
+
set meta["answered"] to now()
|
|
126
|
+
set_meta(id, meta)
|
|
127
|
+
bus_publish("approval.result", meta)
|
|
128
|
+
share nothing as "approval:token:" + id
|
|
129
|
+
give ok
|
|
130
|
+
|
|
131
|
+
-- responder desde la UI (loopback, ya autenticado) o desde la terminal: sin token
|
|
132
|
+
export task answer(id, decision)
|
|
133
|
+
let meta be meta_of(id)
|
|
134
|
+
when meta == nothing
|
|
135
|
+
give false
|
|
136
|
+
when meta["status"] != "pending"
|
|
137
|
+
give false
|
|
138
|
+
let v be when decision then "yes" otherwise "no"
|
|
139
|
+
share v as "approval:answer:" + id
|
|
140
|
+
give true
|
|
141
|
+
|
|
142
|
+
-- responder por link: el token debe coincidir (comparación en tiempo constante) y ser de un solo uso
|
|
143
|
+
export task answer_with_token(id, tok, decision)
|
|
144
|
+
observe "approval:token:" + id as want
|
|
145
|
+
when want == nothing
|
|
146
|
+
give {"ok": false, "why": "expired"}
|
|
147
|
+
when not verify_hmac(id, hmac_sha256(id, tok), want)
|
|
148
|
+
give {"ok": false, "why": "bad token"}
|
|
149
|
+
share nothing as "approval:token:" + id
|
|
150
|
+
let v be when decision then "yes" otherwise "no"
|
|
151
|
+
share v as "approval:answer:" + id
|
|
152
|
+
give {"ok": true, "why": ""}
|
|
153
|
+
|
|
154
|
+
-- pendientes y recientes para la UI (nunca incluye tokens); las resueltas hace más de 1 h se olvidan
|
|
155
|
+
export task list()
|
|
156
|
+
require time
|
|
157
|
+
let out be []
|
|
158
|
+
let keep be []
|
|
159
|
+
each id in index()
|
|
160
|
+
let m be meta_of(id)
|
|
161
|
+
when m != nothing
|
|
162
|
+
when m["status"] != "pending" and contains(m, "answered") and now() - m["answered"] > 3600
|
|
163
|
+
share nothing as "approval:meta:" + id
|
|
164
|
+
otherwise
|
|
165
|
+
set out to append(out, m)
|
|
166
|
+
set keep to append(keep, id)
|
|
167
|
+
when length(keep) != length(index())
|
|
168
|
+
share keep as "approval:index"
|
|
169
|
+
give sort_by(out, (m) => 0 - m["created"])
|
|
170
|
+
|
|
171
|
+
export task pending()
|
|
172
|
+
give where(list(), (m) => m["status"] == "pending")
|
|
173
|
+
|
|
174
|
+
-- página mínima que ve quien abre el link
|
|
175
|
+
export task html_page(title, body)
|
|
176
|
+
give "<!doctype html><meta charset=utf-8><meta name=viewport content='width=device-width,initial-scale=1'><title>" + title + "</title><body style='font:16px/1.5 system-ui;margin:0;display:grid;place-items:center;min-height:100vh;background:#f6f4ef;color:#1d1c1a'><div style='max-width:520px;padding:32px;border:1px solid #d9d4c7;border-radius:6px;background:#fffdf8'><p style='margin:0 0 6px;font:11px/1 ui-monospace,monospace;letter-spacing:.12em;text-transform:uppercase;color:#a33'>lampson</p><h1 style='font-size:22px;margin:0 0 12px'>" + title + "</h1><p style='margin:0'>" + body + "</p></div></body>"
|
package/lib/permission.syn
CHANGED
|
@@ -114,6 +114,17 @@ export task evaluate(name, args, mode)
|
|
|
114
114
|
give {"decision": "deny", "reason": "strict mode (" + act + " MCP server)"}
|
|
115
115
|
give {"decision": "ask", "reason": act + "s MCP server '" + (when contains(args, "name") then text(args["name"]) otherwise "?") + "'" + (when act == "add" then " → runs: " + (when contains(args, "command") then text(args["command"]) otherwise "?") otherwise "")}
|
|
116
116
|
give {"decision": "allow", "reason": "read-only tool"}
|
|
117
|
+
when name == "schedule"
|
|
118
|
+
-- programar = autorizar corridas futuras SIN nadie mirando (un comando, una lámpara, o el agente entero
|
|
119
|
+
-- con un sobre de permisos): humano siempre, incluso en yolo. list/log son lectura.
|
|
120
|
+
let sact be when contains(args, "action") then text(args["action"]) otherwise "list"
|
|
121
|
+
when sact == "list" or sact == "log"
|
|
122
|
+
give {"decision": "allow", "reason": "read-only tool"}
|
|
123
|
+
when mode == "strict"
|
|
124
|
+
give {"decision": "deny", "reason": "strict mode (schedule " + sact + ")"}
|
|
125
|
+
when sact == "add"
|
|
126
|
+
give {"decision": "ask", "reason": "schedules an unattended " + (when contains(args, "kind") then text(args["kind"]) otherwise "?") + " task" + (when contains(args, "permission") then " with permission=" + text(args["permission"]) otherwise " with permission=ask")}
|
|
127
|
+
give {"decision": "ask", "reason": "schedule " + sact + " " + (when contains(args, "id") then text(args["id"]) otherwise "?")}
|
|
117
128
|
when name == "skill" and is_install(args)
|
|
118
129
|
-- instala instrucciones/scripts de terceros FUERA del workspace (~/.agents/skills): humano siempre, incluso en yolo
|
|
119
130
|
when mode == "strict"
|
|
@@ -145,6 +156,13 @@ export task describe_call(name, args)
|
|
|
145
156
|
when act == "remove"
|
|
146
157
|
give "mcp remove " + (when contains(args, "name") then text(args["name"]) otherwise "?")
|
|
147
158
|
give "mcp list"
|
|
159
|
+
when name == "schedule"
|
|
160
|
+
let sact be arg(args, "action", "list")
|
|
161
|
+
when sact == "add"
|
|
162
|
+
let what be arg(args, "kind", "?")
|
|
163
|
+
let body be when what == "lamp" then "lamp " + arg(args, "lamp", "?") + "." + arg(args, "tool", "?") + (when contains(args, "args") then " " + one_line(json_encode(args["args"]), 80) otherwise "") otherwise (when what == "bash" then "$ " + one_line(arg(args, "command", "?"), 120) otherwise "agent " + arg(args, "agent", "build") + ": " + one_line(arg(args, "prompt", "?"), 160))
|
|
164
|
+
give "schedule add «" + arg(args, "name", "?") + "» · " + arg(args, "at", "?") + " · " + body + " · permission " + arg(args, "permission", "ask") + (when contains(args, "notify") then " · notify " + text(args["notify"]) otherwise "")
|
|
165
|
+
give "schedule " + sact + (when contains(args, "id") then " " + text(args["id"]) otherwise "")
|
|
148
166
|
when name == "skill" and is_install(args)
|
|
149
167
|
give "skill install " + text(args["source"]) + " --skill " + text(args["name"]) + " (" + (when contains(args, "scope") then text(args["scope"]) otherwise "global") + ")"
|
|
150
168
|
when name == "bash"
|