lampson 0.2.7 → 0.2.8
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 +2 -1
- package/chat.syn +1 -1
- package/lib/agents.syn +5 -5
- package/lib/fs.syn +226 -0
- package/lib/loop.syn +5 -4
- package/lib/permission.syn +26 -0
- package/lib/prompt.syn +1 -0
- package/lib/tools/fetch.syn +573 -0
- package/lib/tools/url.syn +220 -0
- package/lib/tools.syn +3 -1
- package/package.json +10 -2
- package/public/css/sidebar.css +21 -0
- package/public/js/core.js +1 -1
- package/public/js/tree.js +212 -10
- package/skills/lampson/SKILL.md +8 -0
- package/web.syn +15 -1
package/README.md
CHANGED
|
@@ -95,7 +95,8 @@ plugins were called *lamps*; old `.lampson/lamps/` folders and `LAMP_*` variable
|
|
|
95
95
|
|
|
96
96
|
Sub-agents that work in parallel · skills (`SKILL.md` procedures, anything on [skills.sh](https://skills.sh)
|
|
97
97
|
works) · MCP servers with the JSON you already have · language servers for real go-to-definition · project
|
|
98
|
-
memory it reads back next session · sessions with a readable trace of every step ·
|
|
98
|
+
memory it reads back next session · sessions with a readable trace of every step · web pages fetched as
|
|
99
|
+
Markdown (a tenth of the tokens of raw HTML) · paste a screenshot and ask.
|
|
99
100
|
|
|
100
101
|
## Learn more
|
|
101
102
|
|
package/chat.syn
CHANGED
|
@@ -165,7 +165,7 @@ task print_diff_with(lead, path, d)
|
|
|
165
165
|
|
|
166
166
|
-- tools "instantáneas": la llamada se imprime junto con su resultado, en una sola línea
|
|
167
167
|
task is_instant(name)
|
|
168
|
-
give name == "read" or name == "ls" or name == "find" or name == "grep" or name == "memory" or name == "edit" or name == "write" or name == "todo" or name == "lsp"
|
|
168
|
+
give name == "read" or name == "ls" or name == "find" or name == "grep" or name == "memory" or name == "edit" or name == "write" or name == "todo" or name == "lsp" or name == "fetch"
|
|
169
169
|
|
|
170
170
|
let pending_call be ""
|
|
171
171
|
|
package/lib/agents.syn
CHANGED
|
@@ -40,27 +40,27 @@ 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", "plugin", "schedule", "delegate"],
|
|
43
|
+
"tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "fetch", "bash", "process", "memory", "todo", "skill", "mcp", "plugin", "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
|
},
|
|
47
47
|
"plan": {
|
|
48
|
-
"tools": ["read", "ls", "find", "grep", "lsp", "memory", "todo", "skill", "delegate"],
|
|
48
|
+
"tools": ["read", "ls", "find", "grep", "lsp", "fetch", "memory", "todo", "skill", "delegate"],
|
|
49
49
|
"steps": 30,
|
|
50
50
|
"addendum": "You are in PLAN mode: READ-ONLY. You cannot edit files or run commands (those tools are not available). Investigate the codebase, then answer with a concrete, numbered implementation plan: files to touch, what changes in each, risks, and how to verify. Do not write code beyond short illustrative snippets."
|
|
51
51
|
},
|
|
52
52
|
"review": {
|
|
53
|
-
"tools": ["read", "ls", "find", "grep", "lsp", "bash", "process", "memory", "skill"],
|
|
53
|
+
"tools": ["read", "ls", "find", "grep", "lsp", "fetch", "bash", "process", "memory", "skill"],
|
|
54
54
|
"steps": 25,
|
|
55
55
|
"addendum": "You are a REVIEWER: read-only on files (no write/edit), but you may run commands (tests, linters, git diff). Find real bugs, missing error handling, security issues and violations of the project's conventions. Report findings as a list with file:line, severity and a suggested fix. Do not modify files."
|
|
56
56
|
},
|
|
57
57
|
"explore": {
|
|
58
|
-
"tools": ["read", "ls", "find", "grep", "lsp", "memory", "skill"],
|
|
58
|
+
"tools": ["read", "ls", "find", "grep", "lsp", "fetch", "memory", "skill"],
|
|
59
59
|
"steps": 20,
|
|
60
60
|
"addendum": "You are an EXPLORER: read-only. Locate the code relevant to the question (files, functions, line numbers) and report it concisely with paths and short excerpts. Do not propose changes."
|
|
61
61
|
},
|
|
62
62
|
"worker": {
|
|
63
|
-
"tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "bash", "process", "memory", "todo", "skill"],
|
|
63
|
+
"tools": ["read", "write", "edit", "ls", "find", "grep", "lsp", "fetch", "bash", "process", "memory", "todo", "skill"],
|
|
64
64
|
"steps": 40,
|
|
65
65
|
"addendum": "You are a WORKER sub-agent: implement exactly the scoped task you were given (read, edit, run tests), then report what you changed (files) and how you verified it. Stay inside the scope; if something outside it is needed, report it instead of doing it."
|
|
66
66
|
}
|
package/lib/fs.syn
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
-- lib/fs.syn — operaciones de archivos del explorador web: crear, renombrar, mover, copiar, borrar
|
|
2
|
+
--
|
|
3
|
+
-- El runtime tiene write_file/list_dir/file_info pero NO rename/delete/copy/mkdir (v0.6.14), así que mover,
|
|
4
|
+
-- copiar y borrar corren por el shell POSIX (Git Bash en Windows), igual que plugins.remove. Todo path viene
|
|
5
|
+
-- del navegador y pasa por common.ws(): relativo al workspace, sin `..` ni absolutos (raise instructivo).
|
|
6
|
+
-- Reglas al estilo VS Code: nunca pisar un destino existente; duplicar = "<nombre> copy<.ext>", luego
|
|
7
|
+
-- "<nombre> copy 2<.ext>"…; una carpeta no se mueve/copia dentro de sí misma; la raíz no se toca.
|
|
8
|
+
use "./tools/common.syn" as c
|
|
9
|
+
|
|
10
|
+
task sh_quote(s)
|
|
11
|
+
give "'" + replace_text(s, "'", "'\\''") + "'"
|
|
12
|
+
|
|
13
|
+
-- shell POSIX aunque LAMPSON_SHELL apunte a cmd: mv/cp/rm/mkdir son de Git Bash
|
|
14
|
+
task shell()
|
|
15
|
+
require env("LAMPSON_*")
|
|
16
|
+
require env("OS")
|
|
17
|
+
let is_win be env("OS", "") == "Windows_NT"
|
|
18
|
+
let sh be env("LAMPSON_SHELL", when is_win then "C:\\Program Files\\Git\\bin\\bash.exe" otherwise "bash")
|
|
19
|
+
when is_win and not c.ends_with(lower(sh), "bash.exe")
|
|
20
|
+
set sh to "C:\\Program Files\\Git\\bin\\bash.exe"
|
|
21
|
+
give sh
|
|
22
|
+
|
|
23
|
+
-- corre un comando con cwd = workspace; error legible si falla
|
|
24
|
+
task sh(cmd)
|
|
25
|
+
require exec
|
|
26
|
+
require env("LAMPSON_*")
|
|
27
|
+
require env("OS")
|
|
28
|
+
let r be run(shell(), ["-c", cmd], 30, {"cwd": c.ROOT})
|
|
29
|
+
when r["exit_code"] != 0
|
|
30
|
+
let err be trim(text(r["stderr"]))
|
|
31
|
+
raise(when err == "" then "command failed: " + cmd otherwise err)
|
|
32
|
+
give true
|
|
33
|
+
|
|
34
|
+
-- path del navegador → {rel (como lo ve el modelo, "." = raíz), real ("workspace/…"), info}
|
|
35
|
+
task target(path)
|
|
36
|
+
require file.read("workspace")
|
|
37
|
+
require file.read("workspace/*")
|
|
38
|
+
let real be c.ws(path)
|
|
39
|
+
give {"rel": c.unws(real), "real": real, "info": file_info(real)}
|
|
40
|
+
|
|
41
|
+
task parent_of(rel)
|
|
42
|
+
when not contains(rel, "/")
|
|
43
|
+
give "."
|
|
44
|
+
let parts be split(rel, "/")
|
|
45
|
+
give join(slice(parts, 0, length(parts) - 1), "/")
|
|
46
|
+
|
|
47
|
+
task base_of(rel)
|
|
48
|
+
let parts be split(rel, "/")
|
|
49
|
+
give parts[length(parts) - 1]
|
|
50
|
+
|
|
51
|
+
task join_rel(dir, name)
|
|
52
|
+
give when dir == "." or dir == "" then name otherwise dir + "/" + name
|
|
53
|
+
|
|
54
|
+
task check_name(name)
|
|
55
|
+
let n be trim(text(name))
|
|
56
|
+
when n == "" or n == "." or n == ".." or contains(n, "/") or contains(n, "\\")
|
|
57
|
+
raise("invalid name \"" + text(name) + "\": one path segment, without / or \\")
|
|
58
|
+
when length(find_all(n, "[<>:\"|?*]")) > 0
|
|
59
|
+
raise("invalid name \"" + n + "\": < > : \" | ? * are not allowed in file names")
|
|
60
|
+
give n
|
|
61
|
+
|
|
62
|
+
task must_exist(t)
|
|
63
|
+
when not t["info"]["exists"]
|
|
64
|
+
raise("\"" + t["rel"] + "\" does not exist")
|
|
65
|
+
|
|
66
|
+
task must_not_exist(rel)
|
|
67
|
+
require file.read("workspace")
|
|
68
|
+
require file.read("workspace/*")
|
|
69
|
+
when file_exists(c.ROOT + "/" + rel)
|
|
70
|
+
raise("\"" + rel + "\" already exists")
|
|
71
|
+
|
|
72
|
+
-- ¿`path` es `dir` o está dentro de `dir`?
|
|
73
|
+
task inside(dir, path)
|
|
74
|
+
when dir == "."
|
|
75
|
+
give true
|
|
76
|
+
give path == dir or starts_with(path, dir + "/")
|
|
77
|
+
|
|
78
|
+
task must_be_dir(rel, info)
|
|
79
|
+
when not info["exists"]
|
|
80
|
+
raise("\"" + rel + "\" does not exist")
|
|
81
|
+
when not info["is_dir"]
|
|
82
|
+
raise("\"" + rel + "\" is not a folder")
|
|
83
|
+
|
|
84
|
+
export task mkdir(dir, name)
|
|
85
|
+
require exec
|
|
86
|
+
require env("LAMPSON_*")
|
|
87
|
+
require env("OS")
|
|
88
|
+
require file("workspace")
|
|
89
|
+
require file("workspace/*")
|
|
90
|
+
let t be target(dir)
|
|
91
|
+
let rel be join_rel(t["rel"], check_name(name))
|
|
92
|
+
must_not_exist(rel)
|
|
93
|
+
sh("mkdir -p " + sh_quote(rel))
|
|
94
|
+
give {"path": rel, "is_dir": true}
|
|
95
|
+
|
|
96
|
+
export task create(dir, name, content)
|
|
97
|
+
require file("workspace")
|
|
98
|
+
require file("workspace/*")
|
|
99
|
+
let t be target(dir)
|
|
100
|
+
let rel be join_rel(t["rel"], check_name(name))
|
|
101
|
+
must_not_exist(rel)
|
|
102
|
+
write_file(c.ROOT + "/" + rel, when content == nothing then "" otherwise text(content))
|
|
103
|
+
give {"path": rel, "is_dir": false}
|
|
104
|
+
|
|
105
|
+
export task rename(path, name)
|
|
106
|
+
require exec
|
|
107
|
+
require env("LAMPSON_*")
|
|
108
|
+
require env("OS")
|
|
109
|
+
require file("workspace")
|
|
110
|
+
require file("workspace/*")
|
|
111
|
+
let t be target(path)
|
|
112
|
+
must_exist(t)
|
|
113
|
+
when t["rel"] == "."
|
|
114
|
+
raise("the workspace root cannot be renamed")
|
|
115
|
+
let rel be join_rel(parent_of(t["rel"]), check_name(name))
|
|
116
|
+
when rel == t["rel"]
|
|
117
|
+
give {"path": rel, "is_dir": t["info"]["is_dir"]}
|
|
118
|
+
-- solo cambian mayúsculas: en un FS case-insensitive el destino "existe" → pasar por un temporal
|
|
119
|
+
when lower(rel) == lower(t["rel"])
|
|
120
|
+
let tmp be rel + ".lampson-rename-tmp"
|
|
121
|
+
sh("mv " + sh_quote(t["rel"]) + " " + sh_quote(tmp) + " && mv " + sh_quote(tmp) + " " + sh_quote(rel))
|
|
122
|
+
give {"path": rel, "is_dir": t["info"]["is_dir"]}
|
|
123
|
+
must_not_exist(rel)
|
|
124
|
+
sh("mv " + sh_quote(t["rel"]) + " " + sh_quote(rel))
|
|
125
|
+
give {"path": rel, "is_dir": t["info"]["is_dir"]}
|
|
126
|
+
|
|
127
|
+
export task move(path, to_dir)
|
|
128
|
+
require exec
|
|
129
|
+
require env("LAMPSON_*")
|
|
130
|
+
require env("OS")
|
|
131
|
+
require file("workspace")
|
|
132
|
+
require file("workspace/*")
|
|
133
|
+
let t be target(path)
|
|
134
|
+
must_exist(t)
|
|
135
|
+
when t["rel"] == "."
|
|
136
|
+
raise("the workspace root cannot be moved")
|
|
137
|
+
let d be target(to_dir)
|
|
138
|
+
must_be_dir(d["rel"], d["info"])
|
|
139
|
+
when t["info"]["is_dir"] and inside(t["rel"], d["rel"])
|
|
140
|
+
raise("cannot move a folder into itself")
|
|
141
|
+
let rel be join_rel(d["rel"], base_of(t["rel"]))
|
|
142
|
+
when rel == t["rel"]
|
|
143
|
+
give {"path": rel, "is_dir": t["info"]["is_dir"]}
|
|
144
|
+
must_not_exist(rel)
|
|
145
|
+
sh("mv " + sh_quote(t["rel"]) + " " + sh_quote(rel))
|
|
146
|
+
give {"path": rel, "is_dir": t["info"]["is_dir"]}
|
|
147
|
+
|
|
148
|
+
-- "b.ts" en `dir` → "b copy.ts", "b copy 2.ts"…; carpetas y dotfiles: "sub copy", ".env copy"
|
|
149
|
+
task copy_name(dir, base)
|
|
150
|
+
require file.read("workspace")
|
|
151
|
+
require file.read("workspace/*")
|
|
152
|
+
let stem be base
|
|
153
|
+
let ext be ""
|
|
154
|
+
let parts be split(base, ".")
|
|
155
|
+
when length(parts) > 1 and parts[0] != ""
|
|
156
|
+
set ext to "." + parts[length(parts) - 1]
|
|
157
|
+
set stem to slice(base, 0, length(base) - length(ext))
|
|
158
|
+
let n be 1
|
|
159
|
+
let cand be stem + " copy" + ext
|
|
160
|
+
while file_exists(c.ROOT + "/" + join_rel(dir, cand)) and n < 100
|
|
161
|
+
set n to n + 1
|
|
162
|
+
set cand to stem + " copy " + text(n) + ext
|
|
163
|
+
give cand
|
|
164
|
+
|
|
165
|
+
-- to_dir = nothing → duplicar al lado; otra carpeta → copiar ahí (mismo nombre, o "copy" si ya hay uno)
|
|
166
|
+
export task copy(path, to_dir)
|
|
167
|
+
require exec
|
|
168
|
+
require env("LAMPSON_*")
|
|
169
|
+
require env("OS")
|
|
170
|
+
require file("workspace")
|
|
171
|
+
require file("workspace/*")
|
|
172
|
+
let t be target(path)
|
|
173
|
+
must_exist(t)
|
|
174
|
+
when t["rel"] == "."
|
|
175
|
+
raise("the workspace root cannot be copied")
|
|
176
|
+
let dir be when to_dir == nothing then parent_of(t["rel"]) otherwise target(to_dir)["rel"]
|
|
177
|
+
must_be_dir(dir, file_info(c.ws(dir)))
|
|
178
|
+
when t["info"]["is_dir"] and inside(t["rel"], dir)
|
|
179
|
+
raise("cannot copy a folder into itself")
|
|
180
|
+
let base be base_of(t["rel"])
|
|
181
|
+
let rel be join_rel(dir, base)
|
|
182
|
+
when rel == t["rel"] or file_exists(c.ROOT + "/" + rel)
|
|
183
|
+
set rel to join_rel(dir, copy_name(dir, base))
|
|
184
|
+
sh("cp -r " + sh_quote(t["rel"]) + " " + sh_quote(rel))
|
|
185
|
+
give {"path": rel, "is_dir": t["info"]["is_dir"]}
|
|
186
|
+
|
|
187
|
+
export task remove(path)
|
|
188
|
+
require exec
|
|
189
|
+
require env("LAMPSON_*")
|
|
190
|
+
require env("OS")
|
|
191
|
+
require file("workspace")
|
|
192
|
+
require file("workspace/*")
|
|
193
|
+
let t be target(path)
|
|
194
|
+
must_exist(t)
|
|
195
|
+
when t["rel"] == "."
|
|
196
|
+
raise("the workspace root cannot be deleted")
|
|
197
|
+
when t["rel"] == ".git" or t["rel"] == ".lampson"
|
|
198
|
+
raise("\"" + t["rel"] + "\" is not deleted from the explorer")
|
|
199
|
+
sh("rm -rf " + sh_quote(t["rel"]))
|
|
200
|
+
give {"path": t["rel"], "is_dir": t["info"]["is_dir"]}
|
|
201
|
+
|
|
202
|
+
-- POST /api/fs: {op, path, name?, to?, content?} → {path, is_dir}
|
|
203
|
+
export task apply(b)
|
|
204
|
+
require exec
|
|
205
|
+
require env("LAMPSON_*")
|
|
206
|
+
require env("OS")
|
|
207
|
+
require file("workspace")
|
|
208
|
+
require file("workspace/*")
|
|
209
|
+
let op be when contains(b, "op") then lower(text(b["op"])) otherwise ""
|
|
210
|
+
let path be when contains(b, "path") then text(b["path"]) otherwise "."
|
|
211
|
+
let name be when contains(b, "name") then b["name"] otherwise nothing
|
|
212
|
+
-- (`to` es palabra reservada: `set x to`)
|
|
213
|
+
let dest be when contains(b, "to") then text(b["to"]) otherwise nothing
|
|
214
|
+
when op == "mkdir"
|
|
215
|
+
give mkdir(path, name)
|
|
216
|
+
when op == "create"
|
|
217
|
+
give create(path, name, when contains(b, "content") then b["content"] otherwise "")
|
|
218
|
+
when op == "rename"
|
|
219
|
+
give rename(path, name)
|
|
220
|
+
when op == "move"
|
|
221
|
+
give move(path, when dest == nothing then "." otherwise dest)
|
|
222
|
+
when op == "copy"
|
|
223
|
+
give copy(path, dest)
|
|
224
|
+
when op == "delete"
|
|
225
|
+
give remove(path)
|
|
226
|
+
raise("unknown op '" + op + "' (mkdir, create, rename, move, copy, delete)")
|
package/lib/loop.syn
CHANGED
|
@@ -47,7 +47,7 @@ let REPEAT_HARD be 8
|
|
|
47
47
|
-- seguidas sin ninguna acción (edit/write/bash/process/delegate/mcp). Visto 2026-08-28 con deepseek-v4-pro:
|
|
48
48
|
-- 39 read/ls/find seguidos, 0 ediciones, 419k tokens, dos veces, con las reglas de prompt ignoradas.
|
|
49
49
|
-- A la mitad del tope el resultado lleva un aviso; al tope, read/ls/find/grep se rechazan hasta que actúe.
|
|
50
|
-
let READ_ONLY_TOOLS be ["read", "ls", "find", "grep"]
|
|
50
|
+
let READ_ONLY_TOOLS be ["read", "ls", "find", "grep", "fetch"]
|
|
51
51
|
let ACTION_TOOLS be ["edit", "write", "bash", "process", "delegate"]
|
|
52
52
|
|
|
53
53
|
export task explore_cap()
|
|
@@ -67,7 +67,7 @@ export task explore_verdict(streak, name, cap)
|
|
|
67
67
|
when n >= cap
|
|
68
68
|
give {"streak": n, "note": "", "refuse": true}
|
|
69
69
|
when n == floor(cap / 2)
|
|
70
|
-
give {"streak": n, "note": "\n\n[harness] " + text(n) + " read-only calls in a row without changing anything. You likely know enough: edit/write now, or run the relevant command. After " + text(cap) + " read-only calls in a row, read/ls/find/grep are refused until you act. If the codebase is genuinely too big, delegate ONE focused question to an `explore` sub-agent instead of reading everything yourself.", "refuse": false}
|
|
70
|
+
give {"streak": n, "note": "\n\n[harness] " + text(n) + " read-only calls in a row without changing anything. You likely know enough: edit/write now, or run the relevant command. After " + text(cap) + " read-only calls in a row, read/ls/find/grep/fetch are refused until you act. If the codebase is genuinely too big, delegate ONE focused question to an `explore` sub-agent instead of reading everything yourself.", "refuse": false}
|
|
71
71
|
give {"streak": n, "note": "", "refuse": false}
|
|
72
72
|
|
|
73
73
|
-- JSON canónico (claves ordenadas en profundidad): reordenar propiedades no engaña al detector
|
|
@@ -98,7 +98,8 @@ task safe_id(id, step)
|
|
|
98
98
|
export task spill(name, id, out)
|
|
99
99
|
require file(".lampson")
|
|
100
100
|
require file(".lampson/*")
|
|
101
|
-
|
|
101
|
+
-- read y fetch se recortan solas (fetch: cabeza+cola con el texto completo ya en .lampson/spill)
|
|
102
|
+
when name == "read" or name == "fetch" or length(out) <= SPILL_CAP
|
|
102
103
|
give out
|
|
103
104
|
-- el informe de los subagentes es el entregable: no se manda a disco (hasta 60k)
|
|
104
105
|
when name == "delegate" and length(out) <= SPILL_CAP * 6
|
|
@@ -394,7 +395,7 @@ export task run_turn(cfg, messages, opts, on_event)
|
|
|
394
395
|
set out to "ERROR: you already called " + tc["name"] + " with these exact arguments " + text(repeats) + " times in a row. The result will not change; try a different approach or report the problem."
|
|
395
396
|
emit(on_event, "tool_denied", {"call": tc, "reason": "repeated call (" + text(repeats) + "x)"}, tag)
|
|
396
397
|
otherwise when ex["refuse"]
|
|
397
|
-
set out to "ERROR: exploration cap reached — " + text(explore_streak) + " read-only calls in a row (read/ls/find/grep) without a single change. Reading more will not help. Do one of: (1) edit/write the files you already read; (2) run a command (bash/process) that moves the task; (3) delegate ONE focused question to an `explore` sub-agent; (4) tell the user what you need. read/ls/find/grep are refused until you do."
|
|
398
|
+
set out to "ERROR: exploration cap reached — " + text(explore_streak) + " read-only calls in a row (read/ls/find/grep) without a single change. Reading more will not help. Do one of: (1) edit/write the files you already read; (2) run a command (bash/process) that moves the task; (3) delegate ONE focused question to an `explore` sub-agent; (4) tell the user what you need. read/ls/find/grep/fetch are refused until you do."
|
|
398
399
|
emit(on_event, "tool_denied", {"call": tc, "reason": "exploration cap (" + text(explore_streak) + " read-only calls without acting)"}, tag)
|
|
399
400
|
otherwise when contains(READ_ONLY_TOOLS, tc["name"]) and reads_total >= turn_cap
|
|
400
401
|
set out to "ERROR: this turn already made " + text(reads_total) + " read-only calls (limit " + text(turn_cap) + "). Everything you read is in your context. Act on it now (edit/write/run), delegate ONE focused question to an `explore` sub-agent, or report to the user."
|
package/lib/permission.syn
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
--
|
|
10
10
|
-- Modos (LAMPSON_PERMISSION): "ask" (default) | "yolo" (dangerous → allow) | "strict" (dangerous → deny)
|
|
11
11
|
|
|
12
|
+
use "./tools/url.syn" as u
|
|
13
|
+
|
|
12
14
|
-- Tier 1: nunca. Coincidencia por substring, case-insensitive.
|
|
13
15
|
-- (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
16
|
export let HARDLINE be [
|
|
@@ -69,6 +71,27 @@ export task evaluate(name, args, mode)
|
|
|
69
71
|
give {"decision": "deny", "reason": "strict mode (dangerous: " + danger + ")"}
|
|
70
72
|
give {"decision": "ask", "reason": "dangerous pattern: " + danger}
|
|
71
73
|
give {"decision": "allow", "reason": "command"}
|
|
74
|
+
when name == "fetch"
|
|
75
|
+
-- política de hosts de url.syn (hermes url_safety): secretos en la URL y metadata de la nube se
|
|
76
|
+
-- deniegan SIEMPRE (incluso en yolo); hosts privados/loopback (el dev server del usuario) piden;
|
|
77
|
+
-- lo público se permite. Una URL inválida se permite: la tool devuelve el error explicado.
|
|
78
|
+
let furl be when contains(args, "url") then text(args["url"]) otherwise ""
|
|
79
|
+
let why be u.sensitive(furl)
|
|
80
|
+
when why != nothing
|
|
81
|
+
give {"decision": "deny", "reason": "the URL carries " + why + " — secrets never travel in URLs"}
|
|
82
|
+
let fp be u.parse(furl)
|
|
83
|
+
when not fp["ok"]
|
|
84
|
+
give {"decision": "allow", "reason": "invalid URL (the tool explains)"}
|
|
85
|
+
let cls be u.host_class(fp["host"])
|
|
86
|
+
when cls == "blocked"
|
|
87
|
+
give {"decision": "deny", "reason": "cloud metadata endpoint " + fp["host"]}
|
|
88
|
+
when cls == "private"
|
|
89
|
+
when mode == "yolo"
|
|
90
|
+
give {"decision": "allow", "reason": "yolo mode (private host " + fp["host"] + ")"}
|
|
91
|
+
when mode == "strict"
|
|
92
|
+
give {"decision": "deny", "reason": "strict mode (private host " + fp["host"] + ")"}
|
|
93
|
+
give {"decision": "ask", "reason": "fetches a private/loopback host (" + fp["origin"] + ")"}
|
|
94
|
+
give {"decision": "allow", "reason": "public URL"}
|
|
72
95
|
when name == "write" or name == "edit"
|
|
73
96
|
-- el scope file("./*") ya impide salir del workspace; aquí solo miramos secretos obvios
|
|
74
97
|
let p be when contains(args, "path") then lower(replace_text(text(args["path"]), "\\", "/")) otherwise ""
|
|
@@ -167,6 +190,9 @@ export task describe_call(name, args)
|
|
|
167
190
|
give "skill install " + text(args["source"]) + " --skill " + text(args["name"]) + " (" + (when contains(args, "scope") then text(args["scope"]) otherwise "global") + ")"
|
|
168
191
|
when name == "bash"
|
|
169
192
|
give "$ " + one_line(when contains(args, "command") then text(args["command"]) otherwise "", 160)
|
|
193
|
+
when name == "fetch"
|
|
194
|
+
let ffmt be arg(args, "format", "markdown")
|
|
195
|
+
give "fetch " + one_line(arg(args, "url", "?"), 140) + (when ffmt != "markdown" then " (" + ffmt + ")" otherwise "")
|
|
170
196
|
-- lectura/búsqueda: como lo escribiría un humano en la shell
|
|
171
197
|
when name == "read"
|
|
172
198
|
let rng be when contains(args, "offset") or contains(args, "limit") then " (" + (when contains(args, "offset") then "desde " + text(floor(number(text(args["offset"])))) otherwise "") + (when contains(args, "limit") then " " + text(floor(number(text(args["limit"])))) + " líneas" otherwise "") + ")" otherwise ""
|
package/lib/prompt.syn
CHANGED
|
@@ -53,6 +53,7 @@ Operate like a careful senior engineer: precise, honest, and economical with wor
|
|
|
53
53
|
|
|
54
54
|
# Tools
|
|
55
55
|
- read (offset/limit), ls, find (glob), grep (regex): use them instead of bash with cat/ls/find/grep — line-numbered and cheaper. lsp (symbols/definition/references/hover): the file's structure without reading it, and exact navigation when grep is ambiguous — prefer lsp symbols plus a ranged read over reading a big file whole. edit: targeted replacement. write: create or fully replace.
|
|
56
|
+
- fetch (url, format, max_chars): web pages and HTTP APIs as Markdown — docs, READMEs, issues, changelogs, JSON. Use it instead of bash with curl/wget for anything you will read: it asks the site for Markdown first and strips scripts, styles and navigation, so a page costs a tenth of its HTML. A long page comes back head+tail with the full text saved to a file: read (offset/limit) or grep that file, never fetch the same URL again. Cite the URL when you use its content. Use bash+curl only for POST, custom headers or downloads.
|
|
56
57
|
- bash: shell commands from the workspace root. State does not persist between calls (cd resets) — chain with &&. Check the [exit code: N] marker on every result (it is the last line) and investigate failures before moving on. Never pipe a build or test through head/tail: the output is already truncated for you (the full text is saved to a file whose path you get) and the pipe hides the real exit code. Never run a server or watcher here.
|
|
57
58
|
- process: start/logs/stop long-running commands (dev servers, watchers). Their new log lines arrive automatically with every later tool result — do not sleep or poll. Stop what you started when the task is done.
|
|
58
59
|
- delegate: sub-agents with a fresh context (explore, plan, review, worker); several tasks in one call run in parallel, background=true returns at once and the report arrives later as a message. Give a highly detailed, self-contained brief, say whether it should write code or only research, how to verify, and exactly what to return. Its report is a self-report — verify what matters before telling the user it is done. Its output is not visible to the user: summarise it.
|