@zenera/neo 1.1.20 → 1.1.22

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.
@@ -1,2 +1,2 @@
1
- export declare const CSS = "\n:root {\n --bg: #0f1115; --panel: #161922; --line: #262b36; --fg: #d7dbe3; --dim: #8a92a6;\n --accent: #6ea8fe; --ok: #4ec9a0; --err: #f0776c; --warn: #e2b341; --mono: ui-monospace, SFMono-Regular, Menlo, monospace;\n}\n* { box-sizing: border-box; }\nbody { margin: 0; background: var(--bg); color: var(--fg);\n font: 14px/1.5 system-ui, -apple-system, Segoe UI, sans-serif; }\nheader { padding: 12px 16px; border-bottom: 1px solid var(--line); background: var(--panel);\n display: flex; flex-wrap: wrap; gap: 8px 20px; align-items: baseline; }\nheader h1 { font-size: 15px; margin: 0 12px 0 0; font-weight: 600; }\nheader .kv { color: var(--dim); font-size: 12px; }\nheader .kv b { color: var(--fg); font-weight: 600; }\nmain { display: grid; grid-template-columns: 340px 1fr; height: calc(100vh - 49px); }\naside { border-right: 1px solid var(--line); overflow: auto; background: var(--panel); }\n.pad { padding: 10px; position: sticky; top: 0; background: var(--panel); border-bottom: 1px solid var(--line); }\n#filter { width: 100%; padding: 6px 8px; background: var(--bg); color: var(--fg);\n border: 1px solid var(--line); border-radius: 6px; }\n#timeline { list-style: none; margin: 0; padding: 4px 0; }\n#timeline li { padding: 6px 10px; border-left: 3px solid transparent; cursor: pointer; }\n#timeline li:hover { background: #1c2130; }\n#timeline li.sel { background: #1f2637; border-left-color: var(--accent); }\n#timeline li.covered { opacity: .45; }\n#timeline .row { display: flex; gap: 6px; align-items: baseline; }\n#timeline .t { color: var(--fg); font: 11px var(--mono); min-width: 38px; text-align: right; }\n#timeline .t.same { color: #3d4453; }\n#timeline .dur { margin-left: auto; color: var(--dim); font: 11px var(--mono); }\n#timeline .idx { color: var(--dim); font: 11px var(--mono); min-width: 22px; }\n#timeline .prev { color: var(--dim); font-size: 11px; white-space: nowrap;\n overflow: hidden; text-overflow: ellipsis; padding-left: 44px; }\n/* Branch histories are contiguous in this list, so a coloured edge plus a\n header is enough to stop ten parallel branches reading as one long run. */\n#timeline li.sep { cursor: default; padding: 10px 10px 4px; display: flex; gap: 7px;\n align-items: center; font-size: 11px; letter-spacing: .06em; text-transform: uppercase;\n color: var(--dim); border-left-color: transparent; }\n#timeline li.sep:hover { background: none; }\n#timeline li.sep .dot { width: 8px; height: 8px; border-radius: 2px;\n background: var(--bc, var(--dim)); }\n/* The group header shouts in uppercase; its elapsed time should not. */\n#timeline li.sep .dur { text-transform: none; letter-spacing: 0; }\n/* The branch the join actually waited for. */\n#timeline li.sep.slow { background: #232a38; color: var(--fg); }\n#timeline li.sep.slow .dur { color: var(--fg); }\n#timeline li.branch { padding-left: 20px; border-left-color: var(--bc, var(--line)); }\n#timeline li.branch.sel { border-left-color: var(--bc, var(--accent)); }\n#middle { display: flex; flex-direction: column; overflow: hidden; }\n.tabs { display: flex; gap: 4px; padding: 8px 12px; border-bottom: 1px solid var(--line); }\n.tabs button { background: none; border: 1px solid transparent; color: var(--dim);\n padding: 4px 12px; border-radius: 6px; cursor: pointer; font-size: 13px; }\n.tabs button.on { color: var(--fg); background: var(--panel); border-color: var(--line); }\n#detail { overflow: auto; padding: 16px; flex: 1; }\n#stats { overflow: auto; padding: 16px; flex: 1; }\ntable.st { width: 100%; border-collapse: collapse; font: 12px var(--mono); }\ntable.st th { text-align: right; color: var(--dim); font-weight: 600; padding: 6px 12px;\n border-bottom: 1px solid var(--line); white-space: nowrap; }\ntable.st td { text-align: right; padding: 5px 12px; border-bottom: 1px solid #1d2230;\n white-space: nowrap; }\ntable.st th:first-child, table.st td:first-child { text-align: left; }\ntable.st tbody tr:hover { background: #1c2130; }\ntable.st tbody tr:last-child td { border-bottom: none; }\n/* The total is a different kind of row, not one more of the same: it gets its\n own band so nobody reads it as another model or another agent. */\ntable.st tfoot td { color: var(--fg); font-weight: 600; background: #191e2a;\n border-top: 1px solid #2b3346; border-bottom: none; padding: 7px 12px; }\ntable.st tfoot td:first-child { color: var(--dim); text-transform: uppercase;\n letter-spacing: .07em; font-size: 11px; }\n/* Two panes hold a zoomable diagram (the run graph and the architecture), so\n everything about one is written for both. */\n.pane { display: flex; flex-direction: column; overflow: hidden; flex: 1; }\n/* An explicit display on the pane would otherwise beat the hidden attribute\n and both views would show at once. */\n.pane[hidden], #detail[hidden], #stats[hidden] { display: none; }\n.pane svg { max-width: none; display: block; }\n.pane .node { cursor: pointer; }\n/* A node with nothing behind it in this run is not clickable, and says so. */\n.pane .node.idle { cursor: default; }\n.gtools { display: flex; align-items: center; gap: 6px; padding: 6px 10px;\n border-bottom: 1px solid var(--line); background: var(--panel); }\n.gtools button { background: var(--bg); border: 1px solid var(--line); color: var(--fg);\n border-radius: 6px; padding: 2px 10px; cursor: pointer; font: 12px var(--mono); }\n.gtools button:hover { border-color: var(--accent); }\n.gtools .hint:first-of-type { margin-left: 8px; }\n.gtools .lvl { margin-left: auto; font-family: var(--mono); }\n.gtools label.check { display: flex; align-items: center; gap: 5px; margin-left: 8px;\n color: var(--dim); font-size: 12px; cursor: pointer; user-select: none; }\n/* The memory view reads left to right: the graph is the index, the panel is\n the page. Putting the body under a tab of its own would mean losing the\n picture every time you asked what a node actually says. */\n#memory .split { display: flex; flex: 1; overflow: hidden; }\n#mdetail { width: 380px; flex: none; padding: 16px; overflow: auto;\n border-right: none; border-left: 1px solid var(--line); }\n#mdetail .dt { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 0 0 10px; }\n#mdetail .idrow { display: flex; gap: 8px; align-items: center; margin: 0 0 14px; }\n#mdetail .idrow code { flex: 1; min-width: 0; font: 12px var(--mono); color: var(--fg);\n background: var(--bg); border: 1px solid var(--line); border-radius: 6px;\n padding: 4px 8px; word-break: break-all; }\n#mdetail .idrow button { flex: 0 0 auto; background: var(--bg); border: 1px solid var(--line);\n color: var(--dim); border-radius: 6px; padding: 4px 9px; font: 11px var(--mono);\n cursor: pointer; }\n#mdetail .idrow button:hover { border-color: var(--accent); color: var(--fg); }\n#mdetail dl.props { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 5px 14px;\n margin: 0 0 14px; font: 12px var(--mono); }\n#mdetail dl.props dt { color: var(--dim); }\n#mdetail dl.props dd { margin: 0; word-break: break-word; }\n/* A block that shares the panel's own background has no edges to show. */\n#mdetail section.blk { background: var(--bg); }\n#mdetail ul.links { list-style: none; margin: 0; padding: 0; }\n#mdetail ul.links li { display: grid; grid-template-columns: 96px auto minmax(0, 1fr);\n gap: 8px; align-items: baseline; padding: 6px 12px; }\n#mdetail ul.links li + li { border-top: 1px solid var(--line); }\n#mdetail ul.links .rel { font: 11px var(--mono); color: var(--warn); white-space: nowrap; }\n#mdetail button.link { font: 12px/1.4 system-ui, sans-serif; padding: 0; text-align: left;\n overflow-wrap: anywhere; }\n#mdetail button.link:hover { text-decoration: underline; }\n#mcount .ok { color: var(--ok); }\n#mcount .bad { color: var(--err); }\n/* The kind palette is the memory page's, so a node reads the same in both. */\n.kind { font-size: 11px; letter-spacing: .04em; text-transform: uppercase;\n padding: 1px 6px; border-radius: 4px; background: #232a38; color: var(--dim); }\n.kind.task { background: #1e3357; color: #9dc1ff; }\n.kind.plan { background: #3a2450; color: #d1a6ff; }\n.kind.fact { background: #14392c; color: #8ee0c2; }\n.kind.snippet { background: #1e3d33; color: #8ee0c2; }\n.kind.file { background: #40331a; color: #f0dfb4; }\n.kind.operation { background: #3d2a1b; color: #f2d9c2; }\n.kind.preference { background: #3d1f2c; color: #f5b8cd; }\n/* Selection is a class rather than attributes written onto the shapes: the\n previous pick has to lose its outline without a redraw. White, because the\n accent already means \"the recall matched this one\". */\n#memory .node.picked rect, #memory .node.picked polygon,\n#memory .node.picked path, #memory .node.picked circle {\n stroke: var(--fg) !important; stroke-width: 4px !important; }\n/* The canvas is transformed, not scrolled: panning has to work past the edges\n of the diagram, which overflow:auto would forbid. */\n.viewport { flex: 1; overflow: hidden; position: relative; cursor: grab; touch-action: none;\n user-select: none; -webkit-user-select: none; }\n.viewport.drag { cursor: grabbing; }\n.canvas { position: absolute; top: 0; left: 0; transform-origin: 0 0; }\n.hint { color: var(--dim); font-size: 12px; }\n.hint.pad { padding: 16px; }\n.type { font-size: 11px; letter-spacing: .04em; text-transform: uppercase;\n padding: 1px 6px; border-radius: 4px; background: #232a38; color: var(--dim); }\n.type.llm_call { background: #1e3357; color: #9dc1ff; }\n.type.tool_call, .type.tool_result { background: #1e3d33; color: #8ee0c2; }\n.type.compaction { background: #40331a; color: var(--warn); }\n.type.fork, .type.join { background: #3a2450; color: #d1a6ff; }\n.type.final_output { background: #14392c; color: var(--ok); }\n.type.err { background: #4a1f1c; color: var(--err); }\nh2.dt { font-size: 16px; margin: 0 0 4px; display: flex; gap: 10px; align-items: center; }\n.meta { color: var(--dim); font-size: 12px; font-family: var(--mono); margin-bottom: 14px;\n display: flex; flex-wrap: wrap; gap: 4px 14px; }\nsection.blk { border: 1px solid var(--line); border-radius: 8px; margin-bottom: 12px;\n background: var(--panel); overflow: hidden; }\nsection.blk > h3 { margin: 0; padding: 7px 12px; font-size: 12px; font-weight: 600;\n color: var(--dim); border-bottom: 1px solid var(--line); letter-spacing: .03em; }\nsection.blk > h3 .tag { float: right; font-weight: 400; }\nsection.blk.sys { border-left: 3px solid var(--warn); }\n/* An LLM call has three things a reader wants to tell apart at a glance: what\n went in, what came back, and what it cost. Groups carry the first two. */\n.group { border: 1px solid var(--line); border-radius: 10px; margin-bottom: 14px;\n background: #12151d; overflow: hidden; }\n.group > .ghdr { display: flex; align-items: center; gap: 10px; padding: 8px 12px;\n font-size: 11px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase;\n background: #1b2130; border-bottom: 1px solid var(--line); }\n.group > .ghdr .tag { margin-left: auto; font: 11px var(--mono); font-weight: 400;\n letter-spacing: 0; text-transform: none; color: var(--dim); }\n.group > .body { padding: 10px; }\n.group > .body > .blk:last-child { margin-bottom: 0; }\n.group.req { border-left: 3px solid var(--accent); }\n.group.req > .ghdr { color: #9dc1ff; }\n.group.res { border-left: 3px solid var(--ok); }\n.group.res > .ghdr { color: #8ee0c2; }\n.stats { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; }\n.stat { background: var(--panel); border: 1px solid var(--line); border-radius: 999px;\n padding: 3px 12px; font: 12px var(--mono); color: var(--dim); }\n.stat b { color: var(--fg); font-weight: 600; }\npre { margin: 0; padding: 10px 12px; font: 12px/1.55 var(--mono); white-space: pre-wrap;\n word-break: break-word; overflow-wrap: anywhere; }\npre.text { white-space: pre-wrap; }\n/* Section tags inside a prompt (<house_rules src=\"...\">, <role>, ...): the only\n structure a composed prompt has, and invisible without this. */\npre .xtag { color: #ffb0cd; background: #2d1b27; border: 1px solid #4e2c40;\n border-radius: 4px; padding: 0 4px; font-weight: 600; }\npre .xtag.end { color: #a07f92; background: #201a1e; border-color: #3b2b35;\n font-weight: 400; }\nimg.media { display: block; max-width: 100%; max-height: 420px; margin: 10px 12px;\n border: 1px solid var(--line); border-radius: 6px; background: #0d1017; }\nimg.media.thumb { max-height: 88px; margin: 8px 12px; cursor: zoom-in; }\n.msg { border-top: 1px solid var(--line); border-left: 3px solid transparent; }\n.msg:first-of-type { border-top: none; }\n.msg > .role { padding: 5px 12px; font-size: 11px; text-transform: uppercase;\n letter-spacing: .05em; color: var(--dim); background: #1b202c;\n display: flex; justify-content: space-between; }\n.msg.user > .role { color: #9dc1ff; }\n.msg.user { border-left-color: #35507f; }\n.msg.assistant > .role { color: #c3a6ff; }\n.msg.assistant { border-left-color: #5a4380; }\n.msg.tool > .role { color: #8ee0c2; }\n.msg.tool { border-left-color: #2f6b58; }\n.msg.system > .role { color: var(--warn); }\n.msg.system { border-left-color: #7a6535; }\n.msg.err > .role { color: var(--err); }\n.msg.err { border-left-color: #8a3a33; }\n.call { border-top: 1px dashed var(--line); }\n.call .name { padding: 5px 12px; font: 12px var(--mono); color: #8ee0c2; }\ndetails > summary { cursor: pointer; padding: 6px 12px; color: var(--dim); font-size: 12px; }\n.badge { font: 11px var(--mono); color: var(--dim); }\n.badge.ok { color: var(--ok); } .badge.bad { color: var(--err); }\n.empty { padding: 10px 12px; color: var(--dim); font-size: 12px; font-style: italic; }\nbutton.link { background: none; border: none; color: var(--accent); cursor: pointer;\n font: 12px var(--mono); padding: 0 6px 0 0; }\n";
1
+ export declare const CSS = "\n:root {\n --bg: #0f1115; --panel: #161922; --line: #262b36; --fg: #d7dbe3; --dim: #8a92a6;\n --accent: #6ea8fe; --ok: #4ec9a0; --err: #f0776c; --warn: #e2b341; --mono: ui-monospace, SFMono-Regular, Menlo, monospace;\n}\n* { box-sizing: border-box; }\nbody { margin: 0; background: var(--bg); color: var(--fg);\n font: 14px/1.5 system-ui, -apple-system, Segoe UI, sans-serif; }\nheader { padding: 12px 16px; border-bottom: 1px solid var(--line); background: var(--panel);\n display: flex; flex-wrap: wrap; gap: 8px 20px; align-items: baseline; }\nheader h1 { font-size: 15px; margin: 0 12px 0 0; font-weight: 600; }\nheader .kv { color: var(--dim); font-size: 12px; }\nheader .kv b { color: var(--fg); font-weight: 600; }\nmain { display: grid; grid-template-columns: 340px 1fr; height: calc(100vh - 49px); }\naside { border-right: 1px solid var(--line); overflow: auto; background: var(--panel); }\n.pad { padding: 10px; position: sticky; top: 0; background: var(--panel); border-bottom: 1px solid var(--line); }\n#filter { width: 100%; padding: 6px 8px; background: var(--bg); color: var(--fg);\n border: 1px solid var(--line); border-radius: 6px; }\n#timeline { list-style: none; margin: 0; padding: 4px 0; }\n#timeline li { padding: 6px 10px; border-left: 3px solid transparent; cursor: pointer; }\n#timeline li:hover { background: #1c2130; }\n#timeline li.sel { background: #1f2637; border-left-color: var(--accent); }\n#timeline li.covered { opacity: .45; }\n#timeline .row { display: flex; gap: 6px; align-items: baseline; }\n#timeline .t { color: var(--fg); font: 11px var(--mono); min-width: 38px; text-align: right; }\n#timeline .t.same { color: #3d4453; }\n#timeline .dur { margin-left: auto; color: var(--dim); font: 11px var(--mono); }\n#timeline .idx { color: var(--dim); font: 11px var(--mono); min-width: 22px; }\n#timeline .prev { color: var(--dim); font-size: 11px; white-space: nowrap;\n overflow: hidden; text-overflow: ellipsis; padding-left: 44px; }\n/* Branch histories are contiguous in this list, so a coloured edge plus a\n header is enough to stop ten parallel branches reading as one long run. */\n#timeline li.sep { cursor: default; padding: 10px 10px 4px; display: flex; gap: 7px;\n align-items: center; font-size: 11px; letter-spacing: .06em; text-transform: uppercase;\n color: var(--dim); border-left-color: transparent; }\n#timeline li.sep:hover { background: none; }\n#timeline li.sep .dot { width: 8px; height: 8px; border-radius: 2px;\n background: var(--bc, var(--dim)); }\n/* The group header shouts in uppercase; its elapsed time should not. */\n#timeline li.sep .dur { text-transform: none; letter-spacing: 0; }\n/* The branch the join actually waited for. */\n#timeline li.sep.slow { background: #232a38; color: var(--fg); }\n#timeline li.sep.slow .dur { color: var(--fg); }\n#timeline li.branch { padding-left: 20px; border-left-color: var(--bc, var(--line)); }\n#timeline li.branch.sel { border-left-color: var(--bc, var(--accent)); }\n#middle { display: flex; flex-direction: column; overflow: hidden; }\n.tabs { display: flex; gap: 4px; padding: 8px 12px; border-bottom: 1px solid var(--line); }\n.tabs button { background: none; border: 1px solid transparent; color: var(--dim);\n padding: 4px 12px; border-radius: 6px; cursor: pointer; font-size: 13px; }\n.tabs button.on { color: var(--fg); background: var(--panel); border-color: var(--line); }\n#detail { overflow: auto; padding: 16px; flex: 1; }\n#stats { overflow: auto; padding: 16px; flex: 1; }\ntable.st { width: 100%; border-collapse: collapse; font: 12px var(--mono); }\ntable.st th { text-align: right; color: var(--dim); font-weight: 600; padding: 6px 12px;\n border-bottom: 1px solid var(--line); white-space: nowrap; }\ntable.st td { text-align: right; padding: 5px 12px; border-bottom: 1px solid #1d2230;\n white-space: nowrap; }\ntable.st th:first-child, table.st td:first-child { text-align: left; }\ntable.st tbody tr:hover { background: #1c2130; }\ntable.st tbody tr:last-child td { border-bottom: none; }\n/* The total is a different kind of row, not one more of the same: it gets its\n own band so nobody reads it as another model or another agent. */\ntable.st tfoot td { color: var(--fg); font-weight: 600; background: #191e2a;\n border-top: 1px solid #2b3346; border-bottom: none; padding: 7px 12px; }\ntable.st tfoot td:first-child { color: var(--dim); text-transform: uppercase;\n letter-spacing: .07em; font-size: 11px; }\n/* Two panes hold a zoomable diagram (the run graph and the architecture), so\n everything about one is written for both. */\n.pane { display: flex; flex-direction: column; overflow: hidden; flex: 1; }\n/* An explicit display on the pane would otherwise beat the hidden attribute\n and both views would show at once. */\n.pane[hidden], #detail[hidden], #stats[hidden] { display: none; }\n.pane svg { max-width: none; display: block; }\n.pane .node { cursor: pointer; }\n/* A node with nothing behind it in this run is not clickable, and says so. */\n.pane .node.idle { cursor: default; }\n.gtools { display: flex; align-items: center; gap: 6px; padding: 6px 10px;\n border-bottom: 1px solid var(--line); background: var(--panel); }\n.gtools button { background: var(--bg); border: 1px solid var(--line); color: var(--fg);\n border-radius: 6px; padding: 2px 10px; cursor: pointer; font: 12px var(--mono); }\n.gtools button:hover { border-color: var(--accent); }\n.gtools .hint:first-of-type { margin-left: 8px; }\n.gtools .lvl { margin-left: auto; font-family: var(--mono); }\n.gtools label.check { display: flex; align-items: center; gap: 5px; margin-left: 8px;\n color: var(--dim); font-size: 12px; cursor: pointer; user-select: none; }\n/* The memory view reads left to right: the graph is the index, the panel is\n the page. Putting the body under a tab of its own would mean losing the\n picture every time you asked what a node actually says. */\n#memory .split { display: flex; flex: 1; overflow: hidden; }\n#mdetail { width: 380px; flex: none; padding: 16px; overflow: auto;\n border-right: none; border-left: 1px solid var(--line); }\n#mdetail .dt { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 0 0 10px; }\n#mdetail .idrow { display: flex; gap: 8px; align-items: center; margin: 0 0 14px; }\n#mdetail .idrow code { flex: 1; min-width: 0; font: 12px var(--mono); color: var(--fg);\n background: var(--bg); border: 1px solid var(--line); border-radius: 6px;\n padding: 4px 8px; word-break: break-all; }\n#mdetail .idrow button { flex: 0 0 auto; background: var(--bg); border: 1px solid var(--line);\n color: var(--dim); border-radius: 6px; padding: 4px 9px; font: 11px var(--mono);\n cursor: pointer; }\n#mdetail .idrow button:hover { border-color: var(--accent); color: var(--fg); }\n#mdetail dl.props { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 5px 14px;\n margin: 0 0 14px; font: 12px var(--mono); }\n#mdetail dl.props dt { color: var(--dim); }\n#mdetail dl.props dd { margin: 0; word-break: break-word; }\n/* A block that shares the panel's own background has no edges to show. */\n#mdetail section.blk { background: var(--bg); }\n#mdetail ul.links { list-style: none; margin: 0; padding: 0; }\n#mdetail ul.links li { display: grid; grid-template-columns: 96px auto minmax(0, 1fr);\n gap: 8px; align-items: baseline; padding: 6px 12px; }\n#mdetail ul.links li + li { border-top: 1px solid var(--line); }\n#mdetail ul.links .rel { font: 11px var(--mono); color: var(--warn); white-space: nowrap; }\n#mdetail button.link { font: 12px/1.4 system-ui, sans-serif; padding: 0; text-align: left;\n overflow-wrap: anywhere; }\n#mdetail button.link:hover { text-decoration: underline; }\n#mcount .ok { color: var(--ok); }\n#mcount .bad { color: var(--err); }\n/* The kind palette is the memory page's, so a node reads the same in both. */\n.kind { font-size: 11px; letter-spacing: .04em; text-transform: uppercase;\n padding: 1px 6px; border-radius: 4px; background: #232a38; color: var(--dim); }\n.kind.task { background: #1e3357; color: #9dc1ff; }\n.kind.plan { background: #3a2450; color: #d1a6ff; }\n.kind.fact { background: #14392c; color: #8ee0c2; }\n.kind.snippet { background: #1e3d33; color: #8ee0c2; }\n.kind.file { background: #40331a; color: #f0dfb4; }\n.kind.operation { background: #3d2a1b; color: #f2d9c2; }\n.kind.preference { background: #3d1f2c; color: #f5b8cd; }\n/* Selection is a class rather than attributes written onto the shapes: the\n previous pick has to lose its outline without a redraw. White, because the\n accent already means \"the recall matched this one\". */\n#memory .node.picked rect, #memory .node.picked polygon,\n#memory .node.picked path, #memory .node.picked circle {\n stroke: var(--fg) !important; stroke-width: 4px !important; }\n/* The canvas is transformed, not scrolled: panning has to work past the edges\n of the diagram, which overflow:auto would forbid. */\n.viewport { flex: 1; overflow: hidden; position: relative; cursor: grab; touch-action: none;\n user-select: none; -webkit-user-select: none; }\n.viewport.drag { cursor: grabbing; }\n.canvas { position: absolute; top: 0; left: 0; transform-origin: 0 0; }\n.hint { color: var(--dim); font-size: 12px; }\n.hint.pad { padding: 16px; }\n.type { font-size: 11px; letter-spacing: .04em; text-transform: uppercase;\n padding: 1px 6px; border-radius: 4px; background: #232a38; color: var(--dim); }\n.type.llm_call { background: #1e3357; color: #9dc1ff; }\n.type.tool_call, .type.tool_result { background: #1e3d33; color: #8ee0c2; }\n.type.compaction { background: #40331a; color: var(--warn); }\n.type.fork, .type.join { background: #3a2450; color: #d1a6ff; }\n.type.final_output { background: #14392c; color: var(--ok); }\n.type.err { background: #4a1f1c; color: var(--err); }\nh2.dt { font-size: 16px; margin: 0 0 4px; display: flex; gap: 10px; align-items: center; }\n.meta { color: var(--dim); font-size: 12px; font-family: var(--mono); margin-bottom: 14px;\n display: flex; flex-wrap: wrap; gap: 4px 14px; }\nsection.blk { border: 1px solid var(--line); border-radius: 8px; margin-bottom: 12px;\n background: var(--panel); overflow: hidden; }\nsection.blk > h3 { margin: 0; padding: 7px 12px; font-size: 12px; font-weight: 600;\n color: var(--dim); border-bottom: 1px solid var(--line); letter-spacing: .03em; }\nsection.blk > h3 .tag { float: right; font-weight: 400; }\nsection.blk.sys { border-left: 3px solid var(--warn); }\n/* An LLM call has three things a reader wants to tell apart at a glance: what\n went in, what came back, and what it cost. Groups carry the first two. */\n.group { border: 1px solid var(--line); border-radius: 10px; margin-bottom: 14px;\n background: #12151d; overflow: hidden; }\n.group > .ghdr { display: flex; align-items: center; gap: 10px; padding: 8px 12px;\n font-size: 11px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase;\n background: #1b2130; border-bottom: 1px solid var(--line); }\n.group > .ghdr .tag { margin-left: auto; font: 11px var(--mono); font-weight: 400;\n letter-spacing: 0; text-transform: none; color: var(--dim); }\n.group > .body { padding: 10px; }\n.group > .body > .blk:last-child { margin-bottom: 0; }\n.group.req { border-left: 3px solid var(--accent); }\n.group.req > .ghdr { color: #9dc1ff; }\n.group.res { border-left: 3px solid var(--ok); }\n.group.res > .ghdr { color: #8ee0c2; }\n.stats { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; }\n.stat { background: var(--panel); border: 1px solid var(--line); border-radius: 999px;\n padding: 3px 12px; font: 12px var(--mono); color: var(--dim); }\n.stat b { color: var(--fg); font-weight: 600; }\npre { margin: 0; padding: 10px 12px; font: 12px/1.55 var(--mono); white-space: pre-wrap;\n word-break: break-word; overflow-wrap: anywhere; }\npre.text { white-space: pre-wrap; }\n.tagged-pre { margin: 4px 0; }\n.tagged-pre > pre.text { margin: 0; padding: 6px 12px; }\n/* Section tags inside a prompt (<house_rules src=\"...\">, <role>, ...): the only\n structure a composed prompt has, and invisible without this. */\n.xtag { color: #ffb0cd; background: #2d1b27; border: 1px solid #4e2c40;\n border-radius: 4px; padding: 1px 5px; font-weight: 600; font: 12px var(--mono); }\n.xtag.end { color: #a07f92; background: #201a1e; border-color: #3b2b35;\n font-weight: 400; }\ndetails.xtag-section {\n border: 1px solid var(--line); border-radius: 6px; margin: 6px 12px;\n background: var(--panel);\n}\ndetails.xtag-section > summary {\n cursor: pointer; padding: 6px 10px; font: 12px/1.55 var(--mono);\n color: var(--dim); white-space: nowrap; overflow: hidden;\n text-overflow: ellipsis; user-select: none;\n}\ndetails.xtag-section > summary:hover { background: rgba(255, 255, 255, 0.03); }\ndetails.xtag-section > summary .preview {\n color: var(--dim); margin-left: 8px; font-size: 11px;\n}\ndetails.xtag-section[open] > summary {\n white-space: normal; border-bottom: 1px solid var(--line);\n background: rgba(0, 0, 0, 0.15);\n}\ndetails.xtag-section[open] > summary .preview { display: none; }\ndetails.xtag-section > pre.xtag-body {\n margin: 0; padding: 8px 12px 10px; background: #0d1017;\n border-radius: 0 0 5px 5px; font: 12px/1.55 var(--mono);\n white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere;\n}\nimg.media { display: block; max-width: 100%; max-height: 420px; margin: 10px 12px;\n border: 1px solid var(--line); border-radius: 6px; background: #0d1017; }\nimg.media.thumb { max-height: 88px; margin: 8px 12px; cursor: zoom-in; }\n.msg { border-top: 1px solid var(--line); border-left: 3px solid transparent; }\n.msg:first-of-type { border-top: none; }\n.msg > .role { padding: 5px 12px; font-size: 11px; text-transform: uppercase;\n letter-spacing: .05em; color: var(--dim); background: #1b202c;\n display: flex; justify-content: space-between; }\n.msg.user > .role { color: #9dc1ff; }\n.msg.user { border-left-color: #35507f; }\n.msg.assistant > .role { color: #c3a6ff; }\n.msg.assistant { border-left-color: #5a4380; }\n.msg.tool > .role { color: #8ee0c2; }\n.msg.tool { border-left-color: #2f6b58; }\n.msg.system > .role { color: var(--warn); }\n.msg.system { border-left-color: #7a6535; }\n.msg.err > .role { color: var(--err); }\n.msg.err { border-left-color: #8a3a33; }\n.call { border-top: 1px dashed var(--line); }\n.call .name { padding: 5px 12px; font: 12px var(--mono); color: #8ee0c2; }\ndetails > summary { cursor: pointer; padding: 6px 12px; color: var(--dim); font-size: 12px; }\n.badge { font: 11px var(--mono); color: var(--dim); }\n.badge.ok { color: var(--ok); } .badge.bad { color: var(--err); }\n.empty { padding: 10px 12px; color: var(--dim); font-size: 12px; font-style: italic; }\nbutton.link { background: none; border: none; color: var(--accent); cursor: pointer;\n font: 12px var(--mono); padding: 0 6px 0 0; }\n";
2
2
  //# sourceMappingURL=css.d.ts.map
@@ -183,12 +183,37 @@ section.blk.sys { border-left: 3px solid var(--warn); }
183
183
  pre { margin: 0; padding: 10px 12px; font: 12px/1.55 var(--mono); white-space: pre-wrap;
184
184
  word-break: break-word; overflow-wrap: anywhere; }
185
185
  pre.text { white-space: pre-wrap; }
186
+ .tagged-pre { margin: 4px 0; }
187
+ .tagged-pre > pre.text { margin: 0; padding: 6px 12px; }
186
188
  /* Section tags inside a prompt (<house_rules src="...">, <role>, ...): the only
187
189
  structure a composed prompt has, and invisible without this. */
188
- pre .xtag { color: #ffb0cd; background: #2d1b27; border: 1px solid #4e2c40;
189
- border-radius: 4px; padding: 0 4px; font-weight: 600; }
190
- pre .xtag.end { color: #a07f92; background: #201a1e; border-color: #3b2b35;
190
+ .xtag { color: #ffb0cd; background: #2d1b27; border: 1px solid #4e2c40;
191
+ border-radius: 4px; padding: 1px 5px; font-weight: 600; font: 12px var(--mono); }
192
+ .xtag.end { color: #a07f92; background: #201a1e; border-color: #3b2b35;
191
193
  font-weight: 400; }
194
+ details.xtag-section {
195
+ border: 1px solid var(--line); border-radius: 6px; margin: 6px 12px;
196
+ background: var(--panel);
197
+ }
198
+ details.xtag-section > summary {
199
+ cursor: pointer; padding: 6px 10px; font: 12px/1.55 var(--mono);
200
+ color: var(--dim); white-space: nowrap; overflow: hidden;
201
+ text-overflow: ellipsis; user-select: none;
202
+ }
203
+ details.xtag-section > summary:hover { background: rgba(255, 255, 255, 0.03); }
204
+ details.xtag-section > summary .preview {
205
+ color: var(--dim); margin-left: 8px; font-size: 11px;
206
+ }
207
+ details.xtag-section[open] > summary {
208
+ white-space: normal; border-bottom: 1px solid var(--line);
209
+ background: rgba(0, 0, 0, 0.15);
210
+ }
211
+ details.xtag-section[open] > summary .preview { display: none; }
212
+ details.xtag-section > pre.xtag-body {
213
+ margin: 0; padding: 8px 12px 10px; background: #0d1017;
214
+ border-radius: 0 0 5px 5px; font: 12px/1.55 var(--mono);
215
+ white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere;
216
+ }
192
217
  img.media { display: block; max-width: 100%; max-height: 420px; margin: 10px 12px;
193
218
  border: 1px solid var(--line); border-radius: 6px; background: #0d1017; }
194
219
  img.media.thumb { max-height: 88px; margin: 8px 12px; cursor: zoom-in; }
package/dist/kernel.js CHANGED
@@ -378,11 +378,32 @@ export async function applySystemPrompt(state, reg, env) {
378
378
  }
379
379
  function parseForkArgs(raw) {
380
380
  const args = parseArgs(raw);
381
- const branches = args.branches;
381
+ let branches = args.branches;
382
+ // When a model delegates a single task, it frequently flattens the branch
383
+ // fields directly into the root arguments instead of wrapping them in a
384
+ // 1-element array. Coerce it if name and instructions are present at the root.
385
+ if (!Array.isArray(branches) &&
386
+ typeof args.name === 'string' &&
387
+ typeof args.instructions === 'string') {
388
+ branches = [
389
+ {
390
+ name: args.name,
391
+ instructions: args.instructions,
392
+ agent: typeof args.agent === 'string' ? args.agent : undefined,
393
+ },
394
+ ];
395
+ }
382
396
  // Spelled out rather than "invalid arguments": this string is fed back to
383
397
  // the model as the tool result, and it is the only chance it gets to work
384
398
  // out what to do differently.
385
399
  if (!Array.isArray(branches) || branches.length === 0) {
400
+ if (args.name !== undefined ||
401
+ args.instructions !== undefined ||
402
+ args.agent !== undefined) {
403
+ return ('the "branches" argument is missing; "fork" requires an array of branches: ' +
404
+ '{ "branches": [{ "name": "...", "instructions": "...", "agent": "..." }] }. ' +
405
+ 'Do not pass branch fields at the top level.');
406
+ }
386
407
  return 'the "branches" argument is missing or empty; it must be an array of at least one branch';
387
408
  }
388
409
  const parsed = [];
@@ -8,6 +8,8 @@ export declare const MEMORY_MOUNT = "/memory";
8
8
  export declare const MAX_FILE_BYTES: number;
9
9
  export interface RememberOptions {
10
10
  maxBytes?: number;
11
+ /** Display path as passed to the tool, for reporting errors without leaking the host filesystem */
12
+ path?: string;
11
13
  }
12
14
  /**
13
15
  * Copy `source` into the memory directory under `id` and describe it.
@@ -35,23 +35,24 @@ const MAX_FORMAT_LEN = 16;
35
35
  */
36
36
  export async function rememberFile(dir, source, id, opts = {}) {
37
37
  const maxBytes = opts.maxBytes ?? MAX_FILE_BYTES;
38
- const info = await describe(source);
38
+ const name = opts.path ?? source;
39
+ const info = await describe(source, name);
39
40
  if (!info.isFile()) {
40
- throw new MemoryError(`${source} is not a regular file`, 'only files can be remembered; write a `fact` node describing a directory instead');
41
+ throw new MemoryError(`${name} is not a regular file`, 'only files can be remembered; write a `fact` node describing a directory instead');
41
42
  }
42
43
  if (info.size > maxBytes) {
43
- throw new MemoryError(`${source} is ${info.size} bytes, over the ${maxBytes} byte limit for a remembered file`, 'keep the artifact in the workspace and remember a `snippet` of the part that matters');
44
+ throw new MemoryError(`${name} is ${info.size} bytes, over the ${maxBytes} byte limit for a remembered file`, 'keep the artifact in the workspace and remember a `snippet` of the part that matters');
44
45
  }
45
46
  const bytes = await readFile(source);
46
47
  if (bytes.byteLength > maxBytes) {
47
- throw new MemoryError(`${source} grew past the ${maxBytes} byte limit while being read`);
48
+ throw new MemoryError(`${name} grew past the ${maxBytes} byte limit while being read`);
48
49
  }
49
50
  const format = formatOf(source);
50
- const name = id + (format ? `.${format}` : '');
51
+ const fileName = id + (format ? `.${format}` : '');
51
52
  await mkdir(join(dir, FILES_DIR), { recursive: true });
52
- await writeFile(join(dir, FILES_DIR, name), bytes);
53
+ await writeFile(join(dir, FILES_DIR, fileName), bytes);
53
54
  return {
54
- path: `${MEMORY_MOUNT}/${name}`,
55
+ path: `${MEMORY_MOUNT}/${fileName}`,
55
56
  bytes: bytes.byteLength,
56
57
  sha256: createHash('sha256').update(bytes).digest('hex'),
57
58
  format,
@@ -66,12 +67,12 @@ export function hostPath(dir, file) {
66
67
  export async function forgetFile(dir, file) {
67
68
  await rm(hostPath(dir, file), { force: true });
68
69
  }
69
- async function describe(source) {
70
+ async function describe(source, name = source) {
70
71
  try {
71
72
  return await stat(source);
72
73
  }
73
74
  catch {
74
- throw new MemoryError(`${source} does not exist`, 'remember a file the run actually wrote, by the path the file tools use');
75
+ throw new MemoryError(`${name} does not exist`, 'remember a file the run actually wrote, by the path the file tools use');
75
76
  }
76
77
  }
77
78
  /**
@@ -31,6 +31,7 @@ export interface CommitNode {
31
31
  /** absolute host path of a file to copy in, already resolved by the caller */
32
32
  file?: {
33
33
  source: string;
34
+ path?: string;
34
35
  };
35
36
  expectedRevision?: number;
36
37
  }
@@ -111,7 +111,9 @@ export class MemoryIndex {
111
111
  const written = [];
112
112
  try {
113
113
  for (const item of plan.files) {
114
- const file = await rememberFile(this.store.dir, item.source, item.id);
114
+ const file = await rememberFile(this.store.dir, item.source, item.id, {
115
+ path: item.path,
116
+ });
115
117
  written.push(file);
116
118
  item.target.file = file;
117
119
  }
@@ -239,20 +241,41 @@ export class MemoryIndex {
239
241
  const files = [];
240
242
  const embed = [];
241
243
  for (const [i, node] of nodes.entries()) {
242
- const where = node.ref ?? node.id ?? `nodes[${i}]`;
243
- if ((node.ref === undefined) === (node.id === undefined)) {
244
+ let ref = node.ref;
245
+ let id = node.id;
246
+ const where = ref ?? id ?? `nodes[${i}]`;
247
+ // If the model passed both ref and id with the same value (e.g. ref: "x", id: "x"),
248
+ // and id is not an existing node, treat it as a new node create.
249
+ if (ref !== undefined && id !== undefined) {
250
+ if (ref === id && !this.graph.has(id)) {
251
+ id = undefined;
252
+ }
253
+ else {
254
+ throw new MemoryError(`${where}: give either a ref (to create) or an id (to update), not both`);
255
+ }
256
+ }
257
+ else if (ref === undefined &&
258
+ id !== undefined &&
259
+ !this.graph.has(id) &&
260
+ (node.kind !== undefined || (node.text !== undefined && node.text.trim()))) {
261
+ // Models often assign "id" to new nodes instead of "ref". If the id does not
262
+ // match an existing node in memory, treat it as the local ref for creation.
263
+ ref = id;
264
+ id = undefined;
265
+ }
266
+ if (ref === undefined && id === undefined) {
244
267
  throw new MemoryError(`${where}: give either a ref (to create) or an id (to update), not both`);
245
268
  }
246
269
  const audience = this.#audience(node, opts.writes, where);
247
- if (node.id) {
248
- const existing = this.graph.get(node.id);
270
+ if (id) {
271
+ const existing = this.graph.get(id);
249
272
  if (!existing) {
250
273
  throw new MemoryError(`${where}: no such memory`);
251
274
  }
252
275
  if (node.kind !== undefined) {
253
276
  this.#kind(node.kind, where);
254
277
  }
255
- ids[node.id] = node.id;
278
+ ids[id] = id;
256
279
  const patch = {
257
280
  ...(node.kind !== undefined ? { kind: node.kind } : {}),
258
281
  ...(node.text !== undefined ? { text: node.text } : {}),
@@ -262,18 +285,22 @@ export class MemoryIndex {
262
285
  ? { expectedRevision: node.expectedRevision }
263
286
  : {}),
264
287
  };
265
- planned.push({ existing: true, id: node.id, patch });
288
+ planned.push({ existing: true, id, patch });
266
289
  // Only a text change invalidates the vector; re-embedding on an
267
290
  // audience edit would burn a call to store the same numbers.
268
291
  if (node.text !== undefined && node.text !== existing.text) {
269
- embed.push({ id: node.id, text: node.text });
292
+ embed.push({ id, text: node.text });
270
293
  }
271
294
  if (node.file) {
272
- files.push({ id: node.id, source: node.file.source, target: patch });
295
+ files.push({
296
+ id,
297
+ source: node.file.source,
298
+ path: node.file.path,
299
+ target: patch,
300
+ });
273
301
  }
274
302
  continue;
275
303
  }
276
- const ref = node.ref;
277
304
  if (ref in ids) {
278
305
  throw new MemoryError(`${where}: two nodes share the ref "${ref}"`);
279
306
  }
@@ -281,19 +308,24 @@ export class MemoryIndex {
281
308
  throw new MemoryError(`${where}: a new node needs text`, 'text is the only thing a search can match, so it has to describe the node');
282
309
  }
283
310
  const kind = this.#kind(node.kind, where);
284
- const id = opts.clock.newId();
285
- ids[ref] = id;
311
+ const newId = opts.clock.newId();
312
+ ids[ref] = newId;
286
313
  const draft = {
287
- id,
314
+ id: newId,
288
315
  kind,
289
316
  text: node.text,
290
317
  audience: audience ?? [ALL_AGENTS],
291
318
  ...(node.metadata !== undefined ? { metadata: node.metadata } : {}),
292
319
  };
293
- planned.push({ existing: false, id, draft });
294
- embed.push({ id, text: node.text });
320
+ planned.push({ existing: false, id: newId, draft });
321
+ embed.push({ id: newId, text: node.text });
295
322
  if (node.file) {
296
- files.push({ id, source: node.file.source, target: draft });
323
+ files.push({
324
+ id: newId,
325
+ source: node.file.source,
326
+ path: node.file.path,
327
+ target: draft,
328
+ });
297
329
  }
298
330
  }
299
331
  const resolved = edges.map((edge, i) => {
@@ -161,11 +161,11 @@ export function memoryTools(opts) {
161
161
  properties: {
162
162
  ref: {
163
163
  type: 'string',
164
- description: 'a short name for a new node, used by edges in this call',
164
+ description: 'a short local name for a new node (e.g. "task_1", "file_1"), used by edges in this call. Use ref (not id) when creating new nodes.',
165
165
  },
166
166
  id: {
167
167
  type: 'string',
168
- description: 'to change an existing node instead of adding one',
168
+ description: 'the permanent id of an existing node to update in place (from search/load). Do not use for new nodes; use ref instead.',
169
169
  },
170
170
  kind: { type: 'string', enum: kinds, description: kindHelp },
171
171
  text: {
@@ -173,8 +173,25 @@ export function memoryTools(opts) {
173
173
  description: 'what to remember, in your own words',
174
174
  },
175
175
  file: {
176
- type: 'string',
177
- description: 'path of a file to keep a copy of, for kind "file"',
176
+ oneOf: [
177
+ {
178
+ type: 'string',
179
+ description: 'path of an existing file in the workspace to keep a copy of (e.g. "report.py" or "/workspace/report.py"), for kind "file"',
180
+ },
181
+ {
182
+ type: 'object',
183
+ properties: {
184
+ path: {
185
+ type: 'string',
186
+ description: 'path of an existing file in the workspace',
187
+ },
188
+ },
189
+ required: ['path'],
190
+ additionalProperties: false,
191
+ description: 'file object with path',
192
+ },
193
+ ],
194
+ description: 'path of an existing file in the workspace to keep a copy of (e.g. "report.py" or "/workspace/report.py"), for kind "file"',
178
195
  },
179
196
  ...audience,
180
197
  expected_revision: {
@@ -192,11 +209,11 @@ export function memoryTools(opts) {
192
209
  properties: {
193
210
  from: {
194
211
  type: 'string',
195
- description: 'a ref from this call, or an id',
212
+ description: 'a ref from this call, or an existing node id',
196
213
  },
197
214
  to: {
198
215
  type: 'string',
199
- description: 'a ref from this call, or an id',
216
+ description: 'a ref from this call, or an existing node id',
200
217
  },
201
218
  relation: {
202
219
  type: 'string',
@@ -216,15 +233,28 @@ export function memoryTools(opts) {
216
233
  const opId = memoryOpId(tc.state.runId, tc.callId);
217
234
  let nodes;
218
235
  try {
219
- nodes = args.nodes.map((n) => ({
220
- ref: n.ref,
221
- id: n.id,
222
- kind: n.kind,
223
- text: n.text,
224
- audience: n.audience,
225
- expectedRevision: n.expected_revision,
226
- file: n.file === undefined ? undefined : { source: source(tc, n.file) },
227
- }));
236
+ nodes = args.nodes.map((n) => {
237
+ let fileSpec;
238
+ if (n.file !== undefined) {
239
+ const rawPath = extractFilePath(n.file);
240
+ if (!rawPath.trim()) {
241
+ throw new MemoryError('file path is required', 'pass the path of the file in the workspace to remember, e.g. "report.py"');
242
+ }
243
+ fileSpec = {
244
+ source: source(tc, rawPath),
245
+ path: rawPath,
246
+ };
247
+ }
248
+ return {
249
+ ref: n.ref,
250
+ id: n.id,
251
+ kind: n.kind,
252
+ text: n.text,
253
+ audience: n.audience,
254
+ expectedRevision: n.expected_revision,
255
+ file: fileSpec,
256
+ };
257
+ });
228
258
  }
229
259
  catch (err) {
230
260
  return refusal(err);
@@ -322,11 +352,31 @@ function refusal(err) {
322
352
  }
323
353
  throw err;
324
354
  }
355
+ function extractFilePath(raw) {
356
+ if (typeof raw === 'string') {
357
+ return raw;
358
+ }
359
+ if (raw &&
360
+ typeof raw === 'object' &&
361
+ 'path' in raw &&
362
+ typeof raw.path === 'string') {
363
+ return raw.path;
364
+ }
365
+ return '';
366
+ }
325
367
  function source(tc, path) {
326
368
  if (!tc.services.resolveFile) {
327
369
  throw new MemoryError('this agent cannot remember files', 'commit the node without `file`, or give the agent a workspace');
328
370
  }
329
- return tc.services.resolveFile(path);
371
+ try {
372
+ return tc.services.resolveFile(path);
373
+ }
374
+ catch (err) {
375
+ if (err instanceof MemoryError) {
376
+ throw err;
377
+ }
378
+ throw new MemoryError(err instanceof Error ? err.message : String(err), 'remember a file the run actually wrote, by the path the file tools use');
379
+ }
330
380
  }
331
381
  function changed(index, ids) {
332
382
  return Object.values(ids).flatMap((id) => {
@@ -169,6 +169,10 @@ declare const sandbox: z.ZodObject<{
169
169
  }>>;
170
170
  workdir: z.ZodOptional<z.ZodString>;
171
171
  timeout: z.ZodOptional<z.ZodInt>;
172
+ hardening: z.ZodOptional<z.ZodEnum<{
173
+ strict: "strict";
174
+ standard: "standard";
175
+ }>>;
172
176
  user: z.ZodOptional<z.ZodString>;
173
177
  persist: z.ZodOptional<z.ZodBoolean>;
174
178
  env: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -295,6 +299,10 @@ export declare const projectSchema: z.ZodObject<{
295
299
  }>>;
296
300
  workdir: z.ZodOptional<z.ZodString>;
297
301
  timeout: z.ZodOptional<z.ZodInt>;
302
+ hardening: z.ZodOptional<z.ZodEnum<{
303
+ strict: "strict";
304
+ standard: "standard";
305
+ }>>;
298
306
  user: z.ZodOptional<z.ZodString>;
299
307
  persist: z.ZodOptional<z.ZodBoolean>;
300
308
  env: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -355,6 +363,10 @@ export declare const projectSchema: z.ZodObject<{
355
363
  }>>;
356
364
  workdir: z.ZodOptional<z.ZodString>;
357
365
  timeout: z.ZodOptional<z.ZodInt>;
366
+ hardening: z.ZodOptional<z.ZodEnum<{
367
+ strict: "strict";
368
+ standard: "standard";
369
+ }>>;
358
370
  user: z.ZodOptional<z.ZodString>;
359
371
  persist: z.ZodOptional<z.ZodBoolean>;
360
372
  env: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -263,6 +263,20 @@ const sandbox = z
263
263
  workdir: z.string().min(1).optional(),
264
264
  /** seconds one command may take before it is killed */
265
265
  timeout: z.int().positive().optional(),
266
+ /**
267
+ * How much of the host the container is allowed to be.
268
+ *
269
+ * `standard` (the default) keeps the kernel capabilities and a
270
+ * writable root filesystem, so `apt` and `pip` work — which is what a
271
+ * project whose agents install their own dependencies needs.
272
+ *
273
+ * `strict` drops every capability, mounts the root filesystem
274
+ * read-only with an in-memory `noexec` `/tmp`, runs as an
275
+ * unprivileged uid and turns the network off unless this block asks
276
+ * for one. Nothing can be installed at run time, so the image has to
277
+ * carry everything the agent will need. It requires rootless podman.
278
+ */
279
+ hardening: z.enum(['standard', 'strict']).optional(),
266
280
  /** uid, name or `uid:gid`; unset means the image's own user */
267
281
  user: z.string().min(1).optional(),
268
282
  /** keep the container between sessions instead of removing it */
@@ -234,6 +234,63 @@ function readHouseRules(root) {
234
234
  }
235
235
  return rules;
236
236
  }
237
+ const FILE_TOOLS = [
238
+ 'read_file',
239
+ 'list_dir',
240
+ 'find_files',
241
+ 'write_file',
242
+ 'apply_patch',
243
+ 'move_file',
244
+ 'delete_file',
245
+ ];
246
+ const FILE_WRITE_TOOLS = new Set(['write_file', 'apply_patch', 'move_file', 'delete_file']);
247
+ function activeFileTools(selectors) {
248
+ const active = new Set();
249
+ if (!selectors?.length) {
250
+ return active;
251
+ }
252
+ for (const raw of selectors) {
253
+ const drop = raw.startsWith('-');
254
+ const selector = (drop ? raw.slice(1) : raw).trim();
255
+ if (selector === '*' || selector === 'files:*' || selector === 'workspace:*') {
256
+ for (const t of FILE_TOOLS) {
257
+ if (drop) {
258
+ active.delete(t);
259
+ }
260
+ else {
261
+ active.add(t);
262
+ }
263
+ }
264
+ continue;
265
+ }
266
+ const colon = selector.lastIndexOf(':');
267
+ const name = colon >= 0 ? selector.slice(colon + 1) : selector;
268
+ const group = colon >= 0 ? selector.slice(0, colon) : undefined;
269
+ if (FILE_TOOLS.includes(name)) {
270
+ if (!group || group === 'files' || group === 'workspace') {
271
+ if (drop) {
272
+ active.delete(name);
273
+ }
274
+ else {
275
+ active.add(name);
276
+ }
277
+ }
278
+ }
279
+ }
280
+ return active;
281
+ }
282
+ function hasFileTools(spec) {
283
+ return activeFileTools(spec.tools).size > 0;
284
+ }
285
+ function hasFileWriteTools(spec) {
286
+ const active = activeFileTools(spec.tools);
287
+ for (const t of FILE_WRITE_TOOLS) {
288
+ if (active.has(t)) {
289
+ return true;
290
+ }
291
+ }
292
+ return false;
293
+ }
237
294
  /**
238
295
  * What `requires:` may name, and what each one means. A closed vocabulary
239
296
  * rather than an expression language: these are the capabilities the runtime
@@ -253,6 +310,10 @@ const CAPABILITIES = {
253
310
  memory: (spec) => Boolean(spec.memory),
254
311
  'memory-write': (spec) => spec.memory?.access === 'read-write' || spec.memory?.access === 'full',
255
312
  'memory-forget': (spec) => spec.memory?.access === 'full',
313
+ files: (spec) => hasFileTools(spec),
314
+ 'files-write': (spec) => hasFileWriteTools(spec),
315
+ workspace: (spec) => hasFileTools(spec),
316
+ 'workspace-write': (spec) => hasFileWriteTools(spec),
256
317
  };
257
318
  /**
258
319
  * The document as this agent gets it, or `undefined` when its conditions rule
@@ -23,6 +23,8 @@ export interface SandboxSpec {
23
23
  workdir?: string;
24
24
  /** default seconds a single command may take */
25
25
  timeout?: number;
26
+ /** how much of the host the container is allowed to be; `standard` by default */
27
+ hardening?: SandboxHardening;
26
28
  /** uid, name, or `uid:gid`; unset means the image's own user */
27
29
  user?: string;
28
30
  /** keep the container between sessions rather than removing it */
@@ -42,6 +44,27 @@ export interface SandboxSpec {
42
44
  secrets?: readonly string[];
43
45
  }
44
46
  export type SandboxNetwork = 'bridge' | 'none' | 'host';
47
+ /**
48
+ * Two postures, because they cannot both be the default.
49
+ *
50
+ * `standard` is the one this tool was built around: a container with an init,
51
+ * a pid ceiling and `no-new-privileges`, running an image the agent can still
52
+ * install into. Installing a package is the ordinary use of a shell, so the
53
+ * capabilities stay and the root filesystem stays writable.
54
+ *
55
+ * `strict` is for running code nobody has read. Every capability is dropped,
56
+ * the root filesystem is read-only, the only writable temporary space is an
57
+ * in-memory `/tmp` with `noexec,nosuid,nodev`, the process runs as the host's
58
+ * own unprivileged uid rather than as container root, and the network is off
59
+ * unless the project turns it back on. What that costs is exactly what it
60
+ * removes: `apt`, `pip` and anything else that writes outside the mounts stop
61
+ * working, so a strict project bakes its dependencies into the image instead.
62
+ *
63
+ * What it does *not* do is make the bind mounts go away. `/workspace` is the
64
+ * work itself and stays writable — the profile shrinks the blast radius of a
65
+ * command, it does not pretend the agent has nothing to edit.
66
+ */
67
+ export type SandboxHardening = 'standard' | 'strict';
45
68
  export interface SandboxMount {
46
69
  /** absolute host path */
47
70
  host: string;
@@ -104,6 +127,8 @@ interface Resolved extends Required<Omit<SandboxSpec, 'user'>> {
104
127
  readOnly: boolean;
105
128
  mounts: readonly SandboxMount[];
106
129
  engine: string;
130
+ explicitCpus?: boolean;
131
+ explicitMemory?: boolean;
107
132
  }
108
133
  export interface ExecOptions {
109
134
  cwd?: string;