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/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")
@@ -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
- when contains(args, "path")
153
- give name + " " + text(args["path"])
154
- give name + " " + json_encode(args)
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)"