lampson 0.1.0 → 0.1.2
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/README.md +394 -382
- package/bin/lampson.js +8 -2
- package/chat.syn +1029 -799
- package/lampson.cmd +4 -4
- package/lampson.ps1 +88 -88
- package/lampson.sh +42 -42
- package/lib/agents.syn +471 -471
- package/lib/diff.syn +142 -0
- package/lib/lamps.syn +386 -386
- package/lib/line.syn +365 -0
- package/lib/lsp.syn +503 -503
- package/lib/mcp.syn +403 -403
- package/lib/md.syn +171 -0
- package/lib/permission.syn +189 -154
- package/lib/prompt.syn +75 -75
- package/lib/session.syn +111 -111
- package/lib/skills.syn +179 -179
- package/lib/tools/bash.syn +105 -105
- package/lib/tools/edit.syn +35 -32
- package/lib/tools/memo.syn +148 -148
- package/lib/tools/process.syn +46 -46
- package/lib/tools/write.syn +4 -0
- package/lib/tools.syn +198 -198
- package/lib/tree.syn +59 -59
- package/package.json +2 -2
- package/public/index.html +1268 -1268
- package/skills/lampson/SKILL.md +117 -117
- package/skills/synsema/SKILL.md +15 -2
- package/web.syn +468 -468
package/lib/md.syn
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
-- lib/md.syn — markdown → ANSI para la terminal (sin dependencias)
|
|
2
|
+
-- render(md, color) → texto listo para print. color=false → texto plano legible (sin escapes).
|
|
3
|
+
-- inline(s, color) → solo formato inline (negrita, cursiva, `código`, links, ~~tachado~~).
|
|
4
|
+
-- MIGA: `matches` es full-match; para buscar/reemplazar son `capture`/`find_all`/`replace_re`
|
|
5
|
+
-- (backrefs \1). En strings "..." la barra va simple ("\*", "\d"): "\\d" NO es \d.
|
|
6
|
+
-- Los bloques ``` se copian tal cual (sin parseo inline). La web ya renderiza markdown por su cuenta.
|
|
7
|
+
|
|
8
|
+
let ESC be decode(bytes("1b", "hex"))
|
|
9
|
+
let WIDTH be 72
|
|
10
|
+
|
|
11
|
+
task sgr(color, code, s)
|
|
12
|
+
when not color or s == ""
|
|
13
|
+
give s
|
|
14
|
+
give ESC + "[" + code + "m" + s + ESC + "[0m"
|
|
15
|
+
|
|
16
|
+
task rep(ch, n)
|
|
17
|
+
let out be ""
|
|
18
|
+
while length(out) < n
|
|
19
|
+
set out to out + ch
|
|
20
|
+
give out
|
|
21
|
+
|
|
22
|
+
-- ---------- inline ----------
|
|
23
|
+
|
|
24
|
+
task emphasis(s, color)
|
|
25
|
+
let out be s
|
|
26
|
+
-- links [texto](url) → texto url (antes que la cursiva: la url puede traer _ o *)
|
|
27
|
+
set out to replace_re(out, "\[([^\]]+)\]\(([^)]+)\)", sgr(color, "4", "\1") + sgr(color, "2", " \2"))
|
|
28
|
+
set out to replace_re(out, "\*\*([^*]+)\*\*", sgr(color, "1", "\1"))
|
|
29
|
+
set out to replace_re(out, "__([^_]+)__", sgr(color, "1", "\1"))
|
|
30
|
+
set out to replace_re(out, "~~([^~]+)~~", sgr(color, "9", "\1"))
|
|
31
|
+
-- cursiva: *x* solo si no está pegada a texto/número (2*3 no es cursiva). Dos pasadas:
|
|
32
|
+
-- el grupo \3 consume el separador, así que "*a* *b*" necesita la segunda.
|
|
33
|
+
let pass be 0
|
|
34
|
+
while pass < 2
|
|
35
|
+
set out to replace_re(out, "(^|[^\w*])\*([^*\s][^*]*?)\*($|[^\w*])", "\1" + sgr(color, "3", "\2") + "\3")
|
|
36
|
+
set out to replace_re(out, "(^|[^\w_])_([^_\s][^_]*?)_($|[^\w_])", "\1" + sgr(color, "3", "\2") + "\3")
|
|
37
|
+
set pass to pass + 1
|
|
38
|
+
give out
|
|
39
|
+
|
|
40
|
+
export task inline(s, color)
|
|
41
|
+
-- `código` primero: lo de adentro no se toca
|
|
42
|
+
let parts be split(s, "`")
|
|
43
|
+
when length(parts) < 3
|
|
44
|
+
give emphasis(s, color)
|
|
45
|
+
let out be ""
|
|
46
|
+
let idx be 0
|
|
47
|
+
each p in parts
|
|
48
|
+
when idx == length(parts) - 1 and idx % 2 == 1
|
|
49
|
+
set out to out + "`" + emphasis(p, color)
|
|
50
|
+
otherwise when idx % 2 == 1
|
|
51
|
+
set out to out + sgr(color, "33", p)
|
|
52
|
+
otherwise
|
|
53
|
+
set out to out + emphasis(p, color)
|
|
54
|
+
set idx to idx + 1
|
|
55
|
+
give out
|
|
56
|
+
|
|
57
|
+
-- ---------- bloques ----------
|
|
58
|
+
|
|
59
|
+
task indent_of(line)
|
|
60
|
+
let n be 0
|
|
61
|
+
while n < length(line) and slice(line, n, n + 1) == " "
|
|
62
|
+
set n to n + 1
|
|
63
|
+
give n
|
|
64
|
+
|
|
65
|
+
task table_cells(line)
|
|
66
|
+
let t be trim(line)
|
|
67
|
+
when starts_with(t, "|")
|
|
68
|
+
set t to slice(t, 1, length(t))
|
|
69
|
+
when ends_with(t, "|")
|
|
70
|
+
set t to slice(t, 0, length(t) - 1)
|
|
71
|
+
give apply(trim, split(t, "|"))
|
|
72
|
+
|
|
73
|
+
-- rows: lista de listas de celdas (crudas); la primera es el header. Columnas alineadas al ancho
|
|
74
|
+
-- máximo de cada una (ancho visible = length de inline(c, false), sin escapes ni marcas).
|
|
75
|
+
task table_lines(rows, pre, color)
|
|
76
|
+
let widths be []
|
|
77
|
+
each r in rows
|
|
78
|
+
let i be 0
|
|
79
|
+
each c in r
|
|
80
|
+
when i >= length(widths)
|
|
81
|
+
set widths to append(widths, 0)
|
|
82
|
+
when length(inline(c, false)) > widths[i]
|
|
83
|
+
set widths[i] to length(inline(c, false))
|
|
84
|
+
set i to i + 1
|
|
85
|
+
let out be []
|
|
86
|
+
let ri be 0
|
|
87
|
+
each r in rows
|
|
88
|
+
let cells be []
|
|
89
|
+
let i be 0
|
|
90
|
+
each c in r
|
|
91
|
+
let fill be rep(" ", widths[i] - length(inline(c, false)))
|
|
92
|
+
set cells to append(cells, (when ri == 0 then sgr(color, "1", inline(c, color)) otherwise inline(c, color)) + fill)
|
|
93
|
+
set i to i + 1
|
|
94
|
+
set out to append(out, pre + join(cells, sgr(color, "2", " │ ")))
|
|
95
|
+
when ri == 0
|
|
96
|
+
let segs be []
|
|
97
|
+
each w in widths
|
|
98
|
+
set segs to append(segs, rep("─", w))
|
|
99
|
+
set out to append(out, pre + sgr(color, "2", join(segs, "─┼─")))
|
|
100
|
+
set ri to ri + 1
|
|
101
|
+
give out
|
|
102
|
+
|
|
103
|
+
task fence_top(lang, color)
|
|
104
|
+
let label be when lang != "" then " " + lang + " " otherwise ""
|
|
105
|
+
give sgr(color, "2", "┌──" + label + rep("─", WIDTH - 3 - length(label)))
|
|
106
|
+
|
|
107
|
+
export task render(md, color)
|
|
108
|
+
let pre be " "
|
|
109
|
+
let out be []
|
|
110
|
+
let in_code be false
|
|
111
|
+
let fence be ""
|
|
112
|
+
let table be []
|
|
113
|
+
each line in split(md, "\n")
|
|
114
|
+
let t be trim(line)
|
|
115
|
+
let heading be capture(t, "^(#{1,6}) (.+)$")
|
|
116
|
+
let item be capture(t, "^(\d{1,3})[.)] (.*)$")
|
|
117
|
+
let is_row be starts_with(t, "|") and ends_with(t, "|") and not in_code
|
|
118
|
+
when length(table) > 0 and not is_row
|
|
119
|
+
each l in table_lines(table, pre, color)
|
|
120
|
+
set out to append(out, l)
|
|
121
|
+
set table to []
|
|
122
|
+
when in_code
|
|
123
|
+
when starts_with(t, fence)
|
|
124
|
+
set in_code to false
|
|
125
|
+
set out to append(out, pre + sgr(color, "2", "└" + rep("─", WIDTH - 1)))
|
|
126
|
+
otherwise
|
|
127
|
+
set out to append(out, pre + sgr(color, "2", "│ ") + sgr(color, "36", line))
|
|
128
|
+
otherwise when starts_with(t, "```") or starts_with(t, "~~~")
|
|
129
|
+
set in_code to true
|
|
130
|
+
set fence to slice(t, 0, 3)
|
|
131
|
+
set out to append(out, pre + fence_top(trim(slice(t, 3, length(t))), color))
|
|
132
|
+
otherwise when heading != nothing
|
|
133
|
+
let lvl be length(heading[0])
|
|
134
|
+
let raw be trim(heading[1])
|
|
135
|
+
let title be inline(raw, color)
|
|
136
|
+
set out to append(out, "")
|
|
137
|
+
when lvl == 1
|
|
138
|
+
set out to append(out, pre + sgr(color, "1;36", upper(title)))
|
|
139
|
+
set out to append(out, pre + sgr(color, "36", rep("═", length(raw))))
|
|
140
|
+
otherwise when lvl == 2
|
|
141
|
+
set out to append(out, pre + sgr(color, "1;36", title))
|
|
142
|
+
set out to append(out, pre + sgr(color, "2;36", rep("─", length(raw))))
|
|
143
|
+
otherwise
|
|
144
|
+
set out to append(out, pre + sgr(color, "1", title))
|
|
145
|
+
otherwise when matches(t, "(-{3,}|\*{3,}|_{3,})")
|
|
146
|
+
set out to append(out, pre + sgr(color, "2", rep("─", WIDTH)))
|
|
147
|
+
otherwise when starts_with(t, ">")
|
|
148
|
+
set out to append(out, pre + sgr(color, "2", "▎ ") + sgr(color, "3", inline(trim(slice(t, 1, length(t))), color)))
|
|
149
|
+
otherwise when matches(t, "\|[\s:|-]+\|")
|
|
150
|
+
set table to table
|
|
151
|
+
otherwise when is_row
|
|
152
|
+
set table to append(table, table_cells(t))
|
|
153
|
+
otherwise when starts_with(t, "- ") or starts_with(t, "* ") or starts_with(t, "+ ") or item != nothing
|
|
154
|
+
let ind be rep(" ", indent_of(line))
|
|
155
|
+
let mark be when item != nothing then sgr(color, "36", item[0] + ".") otherwise sgr(color, "36", "•")
|
|
156
|
+
let rest be when item != nothing then item[1] otherwise slice(t, 2, length(t))
|
|
157
|
+
when starts_with(rest, "[ ] ")
|
|
158
|
+
set mark to sgr(color, "2", "☐")
|
|
159
|
+
set rest to slice(rest, 4, length(rest))
|
|
160
|
+
otherwise when starts_with(rest, "[x] ") or starts_with(rest, "[X] ")
|
|
161
|
+
set mark to sgr(color, "32", "☑")
|
|
162
|
+
set rest to slice(rest, 4, length(rest))
|
|
163
|
+
set out to append(out, pre + ind + mark + " " + inline(rest, color))
|
|
164
|
+
otherwise
|
|
165
|
+
set out to append(out, when t == "" then "" otherwise pre + inline(line, color))
|
|
166
|
+
when length(table) > 0
|
|
167
|
+
each l in table_lines(table, pre, color)
|
|
168
|
+
set out to append(out, l)
|
|
169
|
+
when in_code
|
|
170
|
+
set out to append(out, pre + sgr(color, "2", "└" + rep("─", WIDTH - 1)))
|
|
171
|
+
give join(out, "\n")
|
package/lib/permission.syn
CHANGED
|
@@ -1,154 +1,189 @@
|
|
|
1
|
-
-- lib/permission.syn — política de permisos (allow | deny | ask) sobre las tool calls
|
|
2
|
-
--
|
|
3
|
-
-- Mezcla de los tres harnesses:
|
|
4
|
-
-- * evaluate(tool, args, mode) → allow | deny | ask; "ask" suspende hasta que el humano responde.
|
|
5
|
-
-- * 3 niveles — HARDLINE (nunca, ni en yolo), DANGEROUS (pide aprobación; yolo lo pasa; strict lo niega), resto.
|
|
6
|
-
--
|
|
7
|
-
-- Nota Synsema: el aislamiento de FILESYSTEM ya lo da el lenguaje (file("./*") es el workspace,
|
|
8
|
-
-- call_tool intersecta), así que aquí solo miramos el CONTENIDO de los comandos de shell.
|
|
9
|
-
--
|
|
10
|
-
-- Modos (LAMPSON_PERMISSION): "ask" (default) | "yolo" (dangerous → allow) | "strict" (dangerous → deny)
|
|
11
|
-
|
|
12
|
-
-- Tier 1: nunca. Coincidencia por substring, case-insensitive.
|
|
13
|
-
-- (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
|
-
export let HARDLINE be [
|
|
15
|
-
"rm -rf / ", "rm -rf /* ", "rm -fr / ", "rm -rf ~ ", "rm -rf ~/ ", "rm -rf * ", "rm -rf . ", "rm -rf ./ ", "rm -rf .. ",
|
|
16
|
-
"mkfs", "dd if=", ":(){", "fork bomb",
|
|
17
|
-
"format c:", "del /s /q c:\\", "rd /s /q c:\\", "rmdir /s /q c:\\",
|
|
18
|
-
"shutdown", "reboot", "> /dev/sda", "chmod -r 777 /",
|
|
19
|
-
"git push --force origin main", "git push -f origin main", "git push --force origin master"
|
|
20
|
-
]
|
|
21
|
-
|
|
22
|
-
-- Tier 2: pide aprobación (o deny en strict, allow en yolo).
|
|
23
|
-
export let DANGEROUS be [
|
|
24
|
-
"rm -rf", "rm -r", "del /s", "rd /s", "rmdir /s", "remove-item -recurse",
|
|
25
|
-
"git push --force", "git push -f", "git reset --hard", "git clean -f", "git checkout --", "git branch -d",
|
|
26
|
-
"sudo", "curl | sh", "curl | bash", "wget | sh", "| sh", "| bash",
|
|
27
|
-
"drop table", "drop database", "truncate table", "delete from",
|
|
28
|
-
"npm publish", "cargo publish", "pip upload", "twine upload",
|
|
29
|
-
"docker rm", "docker system prune", "kubectl delete",
|
|
30
|
-
"chmod", "chown", "> ~/", "crontab",
|
|
31
|
-
"> .env", ">.env", ">> .env", "tee .env", "> ./.env"
|
|
32
|
-
]
|
|
33
|
-
|
|
34
|
-
task contains_any(haystack, needles)
|
|
35
|
-
each n in needles
|
|
36
|
-
when contains(haystack, lower(n))
|
|
37
|
-
give n
|
|
38
|
-
give nothing
|
|
39
|
-
|
|
40
|
-
-- ¿es un archivo de secretos real? ".env", ".env.local", ".env.production"… sí; ".env.example/.sample/.template" no.
|
|
41
|
-
task is_secret_env(base)
|
|
42
|
-
when base == ".env"
|
|
43
|
-
give true
|
|
44
|
-
when not starts_with(base, ".env.")
|
|
45
|
-
give false
|
|
46
|
-
each ok in [".env.example", ".env.sample", ".env.template", ".env.dist", ".env.test.example"]
|
|
47
|
-
when base == ok
|
|
48
|
-
give false
|
|
49
|
-
give true
|
|
50
|
-
|
|
51
|
-
-- (`and` no cortocircuita en Synsema: indexar args["action"] sin la clave explota → helper con when anidado)
|
|
52
|
-
task is_install(args)
|
|
53
|
-
when not contains(args, "action")
|
|
54
|
-
give false
|
|
55
|
-
give args["action"] == "install"
|
|
56
|
-
|
|
57
|
-
-- evaluate(tool_name, args, mode) → {decision: "allow"|"deny"|"ask", reason}
|
|
58
|
-
export task evaluate(name, args, mode)
|
|
59
|
-
when name == "bash"
|
|
60
|
-
let cmd be (when contains(args, "command") then lower(text(args["command"])) otherwise "") + " "
|
|
61
|
-
let hard be contains_any(cmd, HARDLINE)
|
|
62
|
-
when hard != nothing
|
|
63
|
-
give {"decision": "deny", "reason": "hardline pattern: " + hard}
|
|
64
|
-
let danger be contains_any(cmd, DANGEROUS)
|
|
65
|
-
when danger != nothing
|
|
66
|
-
when mode == "yolo"
|
|
67
|
-
give {"decision": "allow", "reason": "yolo mode (dangerous: " + danger + ")"}
|
|
68
|
-
when mode == "strict"
|
|
69
|
-
give {"decision": "deny", "reason": "strict mode (dangerous: " + danger + ")"}
|
|
70
|
-
give {"decision": "ask", "reason": "dangerous pattern: " + danger}
|
|
71
|
-
give {"decision": "allow", "reason": "command"}
|
|
72
|
-
when name == "write" or name == "edit"
|
|
73
|
-
-- el scope file("./*") ya impide salir del workspace; aquí solo miramos secretos obvios
|
|
74
|
-
let p be when contains(args, "path") then lower(replace_text(text(args["path"]), "\\", "/")) otherwise ""
|
|
75
|
-
let parts be split(p, "/")
|
|
76
|
-
let base be parts[length(parts) - 1]
|
|
77
|
-
when is_secret_env(base)
|
|
78
|
-
when mode == "yolo"
|
|
79
|
-
give {"decision": "allow", "reason": "yolo mode (.env)"}
|
|
80
|
-
give {"decision": "ask", "reason": "writes a .env file (secrets)"}
|
|
81
|
-
give {"decision": "allow", "reason": "workspace write"}
|
|
82
|
-
when starts_with(name, "mcp_")
|
|
83
|
-
-- tool de un server MCP (terceros, efectos fuera del workspace): humano por defecto; yolo permite; strict deniega
|
|
84
|
-
when mode == "yolo"
|
|
85
|
-
give {"decision": "allow", "reason": "yolo mode (MCP tool)"}
|
|
86
|
-
when mode == "strict"
|
|
87
|
-
give {"decision": "deny", "reason": "strict mode (MCP tool)"}
|
|
88
|
-
give {"decision": "ask", "reason": "MCP tool " + name}
|
|
89
|
-
when name == "lsp" and contains(args, "op") and text(args["op"]) == "add"
|
|
90
|
-
-- configurar un language server = el harness va a correr un comando (npx …) del preset: humano siempre
|
|
91
|
-
when mode == "strict"
|
|
92
|
-
give {"decision": "deny", "reason": "strict mode (adds an LSP server)"}
|
|
93
|
-
give {"decision": "ask", "reason": "configures LSP server '" + (when contains(args, "server") then text(args["server"]) otherwise "?") + "' (" + (when contains(args, "scope") then text(args["scope"]) otherwise "global") + ") — it will run the preset command on the first query"}
|
|
94
|
-
when starts_with(name, "lamp_")
|
|
95
|
-
-- tool de una lámpara (código del usuario/agente, un proceso por llamada): como las MCP
|
|
96
|
-
when mode == "yolo"
|
|
97
|
-
give {"decision": "allow", "reason": "yolo mode (lamp tool)"}
|
|
98
|
-
when mode == "strict"
|
|
99
|
-
give {"decision": "deny", "reason": "strict mode (lamp tool)"}
|
|
100
|
-
give {"decision": "ask", "reason": "lamp tool " + name}
|
|
101
|
-
when name == "lamp"
|
|
102
|
-
-- encender una lámpara = autorizar código a correr con las capacidades de su manifiesto: humano SIEMPRE
|
|
103
|
-
let act be when contains(args, "action") then text(args["action"]) otherwise "list"
|
|
104
|
-
when act == "enable" or act == "disable"
|
|
105
|
-
when mode == "strict"
|
|
106
|
-
give {"decision": "deny", "reason": "strict mode (" + act + " lamp)"}
|
|
107
|
-
give {"decision": "ask", "reason": act + "s lamp '" + (when contains(args, "name") then text(args["name"]) otherwise "?") + "'"}
|
|
108
|
-
give {"decision": "allow", "reason": "read-only tool"}
|
|
109
|
-
when name == "mcp"
|
|
110
|
-
-- conectar un server MCP = ejecutar un comando de terceros con env propio: humano siempre, incluso en yolo
|
|
111
|
-
let act be when contains(args, "action") then text(args["action"]) otherwise "list"
|
|
112
|
-
when act == "add" or act == "remove"
|
|
113
|
-
when mode == "strict"
|
|
114
|
-
give {"decision": "deny", "reason": "strict mode (" + act + " MCP server)"}
|
|
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
|
-
give {"decision": "allow", "reason": "read-only tool"}
|
|
117
|
-
when name == "skill" and is_install(args)
|
|
118
|
-
-- instala instrucciones/scripts de terceros FUERA del workspace (~/.agents/skills): humano siempre, incluso en yolo
|
|
119
|
-
when mode == "strict"
|
|
120
|
-
give {"decision": "deny", "reason": "strict mode (installs a third-party skill)"}
|
|
121
|
-
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") + ")"}
|
|
122
|
-
give {"decision": "allow", "reason": "read-only tool"}
|
|
123
|
-
|
|
124
|
-
-- Resumen legible de una tool call, para mostrar al humano antes de aprobar / en el log.
|
|
125
|
-
task one_line(s, max)
|
|
126
|
-
let t be replace_text(replace_text(s, "\r", ""), "\n", " ⏎ ")
|
|
127
|
-
when length(t) > max
|
|
128
|
-
give slice(t, 0, max) + "…"
|
|
129
|
-
give t
|
|
130
|
-
|
|
131
|
-
export task describe_call(name, args)
|
|
132
|
-
when starts_with(name, "mcp_") or starts_with(name, "lamp_")
|
|
133
|
-
give name + " " + one_line(json_encode(args), 160)
|
|
134
|
-
when name == "lsp" and contains(args, "op") and text(args["op"]) == "add"
|
|
135
|
-
give "lsp add " + (when contains(args, "server") then text(args["server"]) otherwise "?") + " (" + (when contains(args, "scope") then text(args["scope"]) otherwise "global") + ")"
|
|
136
|
-
when name == "lsp"
|
|
137
|
-
give "lsp " + (when contains(args, "op") then text(args["op"]) otherwise "?") + " " + (when contains(args, "path") then text(args["path"]) otherwise "?") + (when contains(args, "line") then ":" + text(args["line"]) + (when contains(args, "character") then ":" + text(args["character"]) otherwise "") otherwise "")
|
|
138
|
-
when name == "lamp"
|
|
139
|
-
let lact be when contains(args, "action") then text(args["action"]) otherwise "list"
|
|
140
|
-
give "lamp " + lact + (when lact != "list" then " " + (when contains(args, "name") then text(args["name"]) otherwise "?") otherwise "") + (when lact == "create" and contains(args, "files") then " [" + join(keys(args["files"]), ", ") + "]" otherwise "")
|
|
141
|
-
when name == "mcp"
|
|
142
|
-
let act be when contains(args, "action") then text(args["action"]) otherwise "list"
|
|
143
|
-
when act == "add"
|
|
144
|
-
give "mcp add " + (when contains(args, "name") then text(args["name"]) otherwise "?") + " $ " + one_line(when contains(args, "command") then text(args["command"]) otherwise "?", 120) + " (" + (when contains(args, "scope") then text(args["scope"]) otherwise "global") + ")"
|
|
145
|
-
when act == "remove"
|
|
146
|
-
give "mcp remove " + (when contains(args, "name") then text(args["name"]) otherwise "?")
|
|
147
|
-
give "mcp list"
|
|
148
|
-
when name == "skill" and is_install(args)
|
|
149
|
-
give "skill install " + text(args["source"]) + " --skill " + text(args["name"]) + " (" + (when contains(args, "scope") then text(args["scope"]) otherwise "global") + ")"
|
|
150
|
-
when name == "bash"
|
|
151
|
-
give "$ " + one_line(when contains(args, "command") then text(args["command"]) otherwise "", 160)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
1
|
+
-- lib/permission.syn — política de permisos (allow | deny | ask) sobre las tool calls
|
|
2
|
+
--
|
|
3
|
+
-- Mezcla de los tres harnesses:
|
|
4
|
+
-- * evaluate(tool, args, mode) → allow | deny | ask; "ask" suspende hasta que el humano responde.
|
|
5
|
+
-- * 3 niveles — HARDLINE (nunca, ni en yolo), DANGEROUS (pide aprobación; yolo lo pasa; strict lo niega), resto.
|
|
6
|
+
--
|
|
7
|
+
-- Nota Synsema: el aislamiento de FILESYSTEM ya lo da el lenguaje (file("./*") es el workspace,
|
|
8
|
+
-- call_tool intersecta), así que aquí solo miramos el CONTENIDO de los comandos de shell.
|
|
9
|
+
--
|
|
10
|
+
-- Modos (LAMPSON_PERMISSION): "ask" (default) | "yolo" (dangerous → allow) | "strict" (dangerous → deny)
|
|
11
|
+
|
|
12
|
+
-- Tier 1: nunca. Coincidencia por substring, case-insensitive.
|
|
13
|
+
-- (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
|
+
export let HARDLINE be [
|
|
15
|
+
"rm -rf / ", "rm -rf /* ", "rm -fr / ", "rm -rf ~ ", "rm -rf ~/ ", "rm -rf * ", "rm -rf . ", "rm -rf ./ ", "rm -rf .. ",
|
|
16
|
+
"mkfs", "dd if=", ":(){", "fork bomb",
|
|
17
|
+
"format c:", "del /s /q c:\\", "rd /s /q c:\\", "rmdir /s /q c:\\",
|
|
18
|
+
"shutdown", "reboot", "> /dev/sda", "chmod -r 777 /",
|
|
19
|
+
"git push --force origin main", "git push -f origin main", "git push --force origin master"
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
-- Tier 2: pide aprobación (o deny en strict, allow en yolo).
|
|
23
|
+
export let DANGEROUS be [
|
|
24
|
+
"rm -rf", "rm -r", "del /s", "rd /s", "rmdir /s", "remove-item -recurse",
|
|
25
|
+
"git push --force", "git push -f", "git reset --hard", "git clean -f", "git checkout --", "git branch -d",
|
|
26
|
+
"sudo", "curl | sh", "curl | bash", "wget | sh", "| sh", "| bash",
|
|
27
|
+
"drop table", "drop database", "truncate table", "delete from",
|
|
28
|
+
"npm publish", "cargo publish", "pip upload", "twine upload",
|
|
29
|
+
"docker rm", "docker system prune", "kubectl delete",
|
|
30
|
+
"chmod", "chown", "> ~/", "crontab",
|
|
31
|
+
"> .env", ">.env", ">> .env", "tee .env", "> ./.env"
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
task contains_any(haystack, needles)
|
|
35
|
+
each n in needles
|
|
36
|
+
when contains(haystack, lower(n))
|
|
37
|
+
give n
|
|
38
|
+
give nothing
|
|
39
|
+
|
|
40
|
+
-- ¿es un archivo de secretos real? ".env", ".env.local", ".env.production"… sí; ".env.example/.sample/.template" no.
|
|
41
|
+
task is_secret_env(base)
|
|
42
|
+
when base == ".env"
|
|
43
|
+
give true
|
|
44
|
+
when not starts_with(base, ".env.")
|
|
45
|
+
give false
|
|
46
|
+
each ok in [".env.example", ".env.sample", ".env.template", ".env.dist", ".env.test.example"]
|
|
47
|
+
when base == ok
|
|
48
|
+
give false
|
|
49
|
+
give true
|
|
50
|
+
|
|
51
|
+
-- (`and` no cortocircuita en Synsema: indexar args["action"] sin la clave explota → helper con when anidado)
|
|
52
|
+
task is_install(args)
|
|
53
|
+
when not contains(args, "action")
|
|
54
|
+
give false
|
|
55
|
+
give args["action"] == "install"
|
|
56
|
+
|
|
57
|
+
-- evaluate(tool_name, args, mode) → {decision: "allow"|"deny"|"ask", reason}
|
|
58
|
+
export task evaluate(name, args, mode)
|
|
59
|
+
when name == "bash"
|
|
60
|
+
let cmd be (when contains(args, "command") then lower(text(args["command"])) otherwise "") + " "
|
|
61
|
+
let hard be contains_any(cmd, HARDLINE)
|
|
62
|
+
when hard != nothing
|
|
63
|
+
give {"decision": "deny", "reason": "hardline pattern: " + hard}
|
|
64
|
+
let danger be contains_any(cmd, DANGEROUS)
|
|
65
|
+
when danger != nothing
|
|
66
|
+
when mode == "yolo"
|
|
67
|
+
give {"decision": "allow", "reason": "yolo mode (dangerous: " + danger + ")"}
|
|
68
|
+
when mode == "strict"
|
|
69
|
+
give {"decision": "deny", "reason": "strict mode (dangerous: " + danger + ")"}
|
|
70
|
+
give {"decision": "ask", "reason": "dangerous pattern: " + danger}
|
|
71
|
+
give {"decision": "allow", "reason": "command"}
|
|
72
|
+
when name == "write" or name == "edit"
|
|
73
|
+
-- el scope file("./*") ya impide salir del workspace; aquí solo miramos secretos obvios
|
|
74
|
+
let p be when contains(args, "path") then lower(replace_text(text(args["path"]), "\\", "/")) otherwise ""
|
|
75
|
+
let parts be split(p, "/")
|
|
76
|
+
let base be parts[length(parts) - 1]
|
|
77
|
+
when is_secret_env(base)
|
|
78
|
+
when mode == "yolo"
|
|
79
|
+
give {"decision": "allow", "reason": "yolo mode (.env)"}
|
|
80
|
+
give {"decision": "ask", "reason": "writes a .env file (secrets)"}
|
|
81
|
+
give {"decision": "allow", "reason": "workspace write"}
|
|
82
|
+
when starts_with(name, "mcp_")
|
|
83
|
+
-- tool de un server MCP (terceros, efectos fuera del workspace): humano por defecto; yolo permite; strict deniega
|
|
84
|
+
when mode == "yolo"
|
|
85
|
+
give {"decision": "allow", "reason": "yolo mode (MCP tool)"}
|
|
86
|
+
when mode == "strict"
|
|
87
|
+
give {"decision": "deny", "reason": "strict mode (MCP tool)"}
|
|
88
|
+
give {"decision": "ask", "reason": "MCP tool " + name}
|
|
89
|
+
when name == "lsp" and contains(args, "op") and text(args["op"]) == "add"
|
|
90
|
+
-- configurar un language server = el harness va a correr un comando (npx …) del preset: humano siempre
|
|
91
|
+
when mode == "strict"
|
|
92
|
+
give {"decision": "deny", "reason": "strict mode (adds an LSP server)"}
|
|
93
|
+
give {"decision": "ask", "reason": "configures LSP server '" + (when contains(args, "server") then text(args["server"]) otherwise "?") + "' (" + (when contains(args, "scope") then text(args["scope"]) otherwise "global") + ") — it will run the preset command on the first query"}
|
|
94
|
+
when starts_with(name, "lamp_")
|
|
95
|
+
-- tool de una lámpara (código del usuario/agente, un proceso por llamada): como las MCP
|
|
96
|
+
when mode == "yolo"
|
|
97
|
+
give {"decision": "allow", "reason": "yolo mode (lamp tool)"}
|
|
98
|
+
when mode == "strict"
|
|
99
|
+
give {"decision": "deny", "reason": "strict mode (lamp tool)"}
|
|
100
|
+
give {"decision": "ask", "reason": "lamp tool " + name}
|
|
101
|
+
when name == "lamp"
|
|
102
|
+
-- encender una lámpara = autorizar código a correr con las capacidades de su manifiesto: humano SIEMPRE
|
|
103
|
+
let act be when contains(args, "action") then text(args["action"]) otherwise "list"
|
|
104
|
+
when act == "enable" or act == "disable"
|
|
105
|
+
when mode == "strict"
|
|
106
|
+
give {"decision": "deny", "reason": "strict mode (" + act + " lamp)"}
|
|
107
|
+
give {"decision": "ask", "reason": act + "s lamp '" + (when contains(args, "name") then text(args["name"]) otherwise "?") + "'"}
|
|
108
|
+
give {"decision": "allow", "reason": "read-only tool"}
|
|
109
|
+
when name == "mcp"
|
|
110
|
+
-- conectar un server MCP = ejecutar un comando de terceros con env propio: humano siempre, incluso en yolo
|
|
111
|
+
let act be when contains(args, "action") then text(args["action"]) otherwise "list"
|
|
112
|
+
when act == "add" or act == "remove"
|
|
113
|
+
when mode == "strict"
|
|
114
|
+
give {"decision": "deny", "reason": "strict mode (" + act + " MCP server)"}
|
|
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
|
+
give {"decision": "allow", "reason": "read-only tool"}
|
|
117
|
+
when name == "skill" and is_install(args)
|
|
118
|
+
-- instala instrucciones/scripts de terceros FUERA del workspace (~/.agents/skills): humano siempre, incluso en yolo
|
|
119
|
+
when mode == "strict"
|
|
120
|
+
give {"decision": "deny", "reason": "strict mode (installs a third-party skill)"}
|
|
121
|
+
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") + ")"}
|
|
122
|
+
give {"decision": "allow", "reason": "read-only tool"}
|
|
123
|
+
|
|
124
|
+
-- Resumen legible de una tool call, para mostrar al humano antes de aprobar / en el log.
|
|
125
|
+
task one_line(s, max)
|
|
126
|
+
let t be replace_text(replace_text(s, "\r", ""), "\n", " ⏎ ")
|
|
127
|
+
when length(t) > max
|
|
128
|
+
give slice(t, 0, max) + "…"
|
|
129
|
+
give t
|
|
130
|
+
|
|
131
|
+
export task describe_call(name, args)
|
|
132
|
+
when starts_with(name, "mcp_") or starts_with(name, "lamp_")
|
|
133
|
+
give name + " " + one_line(json_encode(args), 160)
|
|
134
|
+
when name == "lsp" and contains(args, "op") and text(args["op"]) == "add"
|
|
135
|
+
give "lsp add " + (when contains(args, "server") then text(args["server"]) otherwise "?") + " (" + (when contains(args, "scope") then text(args["scope"]) otherwise "global") + ")"
|
|
136
|
+
when name == "lsp"
|
|
137
|
+
give "lsp " + (when contains(args, "op") then text(args["op"]) otherwise "?") + " " + (when contains(args, "path") then text(args["path"]) otherwise "?") + (when contains(args, "line") then ":" + text(args["line"]) + (when contains(args, "character") then ":" + text(args["character"]) otherwise "") otherwise "")
|
|
138
|
+
when name == "lamp"
|
|
139
|
+
let lact be when contains(args, "action") then text(args["action"]) otherwise "list"
|
|
140
|
+
give "lamp " + lact + (when lact != "list" then " " + (when contains(args, "name") then text(args["name"]) otherwise "?") otherwise "") + (when lact == "create" and contains(args, "files") then " [" + join(keys(args["files"]), ", ") + "]" otherwise "")
|
|
141
|
+
when name == "mcp"
|
|
142
|
+
let act be when contains(args, "action") then text(args["action"]) otherwise "list"
|
|
143
|
+
when act == "add"
|
|
144
|
+
give "mcp add " + (when contains(args, "name") then text(args["name"]) otherwise "?") + " $ " + one_line(when contains(args, "command") then text(args["command"]) otherwise "?", 120) + " (" + (when contains(args, "scope") then text(args["scope"]) otherwise "global") + ")"
|
|
145
|
+
when act == "remove"
|
|
146
|
+
give "mcp remove " + (when contains(args, "name") then text(args["name"]) otherwise "?")
|
|
147
|
+
give "mcp list"
|
|
148
|
+
when name == "skill" and is_install(args)
|
|
149
|
+
give "skill install " + text(args["source"]) + " --skill " + text(args["name"]) + " (" + (when contains(args, "scope") then text(args["scope"]) otherwise "global") + ")"
|
|
150
|
+
when name == "bash"
|
|
151
|
+
give "$ " + one_line(when contains(args, "command") then text(args["command"]) otherwise "", 160)
|
|
152
|
+
-- lectura/búsqueda: como lo escribiría un humano en la shell
|
|
153
|
+
when name == "read"
|
|
154
|
+
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 ""
|
|
155
|
+
give "read " + arg(args, "path", ".") + rng
|
|
156
|
+
when name == "ls"
|
|
157
|
+
give "ls " + arg(args, "path", ".")
|
|
158
|
+
when name == "find"
|
|
159
|
+
give "find " + arg(args, "pattern", "*") + (when contains(args, "path") then " en " + text(args["path"]) otherwise "")
|
|
160
|
+
when name == "grep"
|
|
161
|
+
let flags be (when contains(args, "regex") and args["regex"] == true then " -E" otherwise "") + (when contains(args, "glob") then " --glob " + text(args["glob"]) otherwise "")
|
|
162
|
+
give "grep" + flags + " \"" + one_line(arg(args, "pattern", ""), 80) + "\"" + (when contains(args, "path") then " " + text(args["path"]) otherwise "")
|
|
163
|
+
when name == "edit"
|
|
164
|
+
give "edit " + arg(args, "path", "?")
|
|
165
|
+
when name == "write"
|
|
166
|
+
give "write " + arg(args, "path", "?") + (when contains(args, "content") then dim_len(text(args["content"])) otherwise "")
|
|
167
|
+
when name == "memory"
|
|
168
|
+
let mact be arg(args, "action", "list")
|
|
169
|
+
give "memory " + mact + (when contains(args, "name") then " " + text(args["name"]) otherwise "") + (when (mact == "write" or mact == "append") and contains(args, "content") then " — " + one_line(text(args["content"]), 70) otherwise "")
|
|
170
|
+
when name == "process"
|
|
171
|
+
let pact be arg(args, "action", "list")
|
|
172
|
+
give "process " + pact + (when contains(args, "name") then " " + text(args["name"]) otherwise "") + (when contains(args, "command") then " $ " + one_line(text(args["command"]), 100) otherwise "")
|
|
173
|
+
when name == "skill"
|
|
174
|
+
give "skill " + arg(args, "action", "load") + " " + arg(args, "name", "?")
|
|
175
|
+
when name == "todo"
|
|
176
|
+
let n be when contains(args, "items") then length(args["items"]) otherwise 0
|
|
177
|
+
let done be when contains(args, "items") then length(where(args["items"], (i) => contains(i, "status") and text(i["status"]) == "done")) otherwise 0
|
|
178
|
+
give "todo " + text(done) + "/" + text(n) + " hechas"
|
|
179
|
+
when contains(args, "path")
|
|
180
|
+
give name + " " + text(args["path"])
|
|
181
|
+
give name + " " + one_line(json_encode(args), 160)
|
|
182
|
+
|
|
183
|
+
task arg(args, key, default)
|
|
184
|
+
when contains(args, key)
|
|
185
|
+
give text(args[key])
|
|
186
|
+
give default
|
|
187
|
+
|
|
188
|
+
task dim_len(s)
|
|
189
|
+
give " (" + text(length(split(s, "\n"))) + " líneas)"
|