hop2 0.11.13 → 0.11.14
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 +31 -0
- package/hay/apps/web/src/App.tsx +96 -11
- package/hay/apps/web/src/components/SessionSwitcher.tsx +47 -2
- package/hay/apps/web/src/components/ViewsPanel.tsx +243 -0
- package/hay/apps/web/src/styles.css +109 -0
- package/hay/apps/web/src/utils/fileDrop.ts +19 -3
- package/hay/apps/web/src/utils/switcherModel.ts +12 -0
- package/hay/apps/web/test/SessionSwitcher.test.tsx +19 -0
- package/hay/apps/web/test/ViewsPanel.test.tsx +118 -0
- package/hay/apps/web/test/fileDrop.test.ts +25 -1
- package/hay-web/assets/digest-archive.json +1024 -292
- package/hay-web/assets/index-Bcl-2CbE.js +133 -0
- package/hay-web/assets/index-Bcl-2CbE.js.map +1 -0
- package/hay-web/assets/{index-DHotLq-D.css → index-DKWcSY-E.css} +1 -1
- package/hay-web/index.html +2 -2
- package/hop +629 -63
- package/package.json +1 -1
- package/skills/hop-session/SKILL.md +32 -11
- package/tests/terminal-api.integration.test.cjs +84 -0
- package/hay-web/assets/index--8sYRyoP.js +0 -133
- package/hay-web/assets/index--8sYRyoP.js.map +0 -1
- package/hay-web/assets/index-D4EXRUpb.css +0 -1
- package/hay-web/assets/index-DnSA-VR6.js +0 -133
- package/hay-web/assets/index-DnSA-VR6.js.map +0 -1
- package/hay-web/assets/index-DzlPn2In.js +0 -133
- package/hay-web/assets/index-DzlPn2In.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hop-session
|
|
3
|
-
description: Conventions for agents working inside a hop terminal session (the HOP_SESSION environment variable is set). Humans watch these terminals live, often from a phone. Covers rendering math with hop math,
|
|
3
|
+
description: Conventions for agents working inside a hop terminal session (the HOP_SESSION environment variable is set). Humans watch these terminals live, often from a phone. Covers rendering math with hop math, handing over results a terminal cannot render — plots, PDFs, images, rendered markdown write-ups — with hop view (always with --title), exposing a running local web server with hop port, ringing the bell for attention, and phone-friendly output.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Working in a hop terminal
|
|
@@ -24,21 +24,42 @@ formula in your response; do not repeat the raw LaTeX next to it. When you
|
|
|
24
24
|
need the rendered text inside a file or message, capture it:
|
|
25
25
|
`hop math '<latex>'` and copy the command's output verbatim.
|
|
26
26
|
|
|
27
|
-
## Showing
|
|
27
|
+
## Showing results a terminal cannot render (plots, PDFs, write-ups)
|
|
28
28
|
|
|
29
|
-
When the deliverable is a plot, a figure,
|
|
30
|
-
|
|
29
|
+
When the deliverable is a plot, a figure, a report, a PDF, an image, or a
|
|
30
|
+
write-up worth reading as a document, do not describe it — publish it and
|
|
31
|
+
hand over the link:
|
|
31
32
|
|
|
32
33
|
```bash
|
|
33
|
-
hop view results/roc_curve.png
|
|
34
|
-
hop view report.html analysis.pdf
|
|
34
|
+
hop view --title "ROC curve: new model vs baseline" results/roc_curve.png
|
|
35
|
+
hop view --title "Q3 regression analysis" report.html analysis.pdf
|
|
35
36
|
```
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
**Always pass `--title`.** It is what the human sees in the session's Views
|
|
39
|
+
list and in the "new result" chip — a filename like `roc_curve.png` makes
|
|
40
|
+
them open it to find out what it is; a title means they already know. One
|
|
41
|
+
title describes the whole handoff, so publishing three plots under
|
|
42
|
+
"before/after/diff for the cache change" reads correctly.
|
|
43
|
+
|
|
44
|
+
Markdown is a first-class result, and you can pipe it straight from stdout
|
|
45
|
+
with no temp file — it is **rendered** (headings, tables, fenced code,
|
|
46
|
+
quotes), not dumped as raw text:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
generate_summary | hop view --name findings.md --title "What the profiler found" -
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Reach for this whenever the answer is long enough that reading it in a
|
|
53
|
+
terminal is worse than reading it as a page: comparisons, tables of numbers,
|
|
54
|
+
anything with structure. A table in the scrollback of a phone terminal is
|
|
55
|
+
close to unreadable; the same table as a view is not.
|
|
56
|
+
|
|
57
|
+
`hop view` copies the file behind hop's auth and prints a `View:` URL. It
|
|
58
|
+
rings the bell once for you, since a published result is usually the thing
|
|
59
|
+
the human is waiting on, and the session's Views surface shows it as new.
|
|
60
|
+
Re-running after updating the file re-publishes under the same link.
|
|
61
|
+
`hop view --list` shows what this session has published; `hop view --rm
|
|
62
|
+
<name>` unpublishes.
|
|
42
63
|
|
|
43
64
|
## Showing a running web server (dev server, local app)
|
|
44
65
|
|
|
@@ -734,3 +734,87 @@ test('an idle agent terminal attach is reaped after the TTL', async () => {
|
|
|
734
734
|
await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions/delete',
|
|
735
735
|
{ name: 'idle-agent-probe', internalName: s.internalName });
|
|
736
736
|
});
|
|
737
|
+
|
|
738
|
+
// Dropping a file into the web terminal uploads its BYTES (a browser never
|
|
739
|
+
// reveals the real path) and hands back a host path the session can open.
|
|
740
|
+
// The filename is attacker-shaped input on its way to becoming a path.
|
|
741
|
+
function requestUpload(port, secret, reqPath, buffer) {
|
|
742
|
+
return new Promise((resolve, reject) => {
|
|
743
|
+
const req = http.request({
|
|
744
|
+
hostname: '127.0.0.1',
|
|
745
|
+
port,
|
|
746
|
+
path: reqPath,
|
|
747
|
+
method: 'POST',
|
|
748
|
+
headers: {
|
|
749
|
+
Authorization: `Bearer ${secret}`,
|
|
750
|
+
'Content-Type': 'application/octet-stream',
|
|
751
|
+
'Content-Length': buffer.length
|
|
752
|
+
}
|
|
753
|
+
}, (res) => {
|
|
754
|
+
let data = '';
|
|
755
|
+
res.on('data', (chunk) => data += chunk);
|
|
756
|
+
res.on('end', () => {
|
|
757
|
+
try { resolve({ status: res.statusCode, data: data ? JSON.parse(data) : null }); }
|
|
758
|
+
catch (e) { resolve({ status: res.statusCode, data }); }
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
req.on('error', reject);
|
|
762
|
+
req.end(buffer);
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
test('a dropped file uploads to host staging and never escapes it', async () => {
|
|
767
|
+
const create = await requestJson(state.port, state.sessionSecret, 'POST', '/api/terminals', {
|
|
768
|
+
name: 'upload-probe',
|
|
769
|
+
cwd: tempDir
|
|
770
|
+
}, agentHeaders);
|
|
771
|
+
assert.equal(create.status, 200);
|
|
772
|
+
const session = create.data.sessionName;
|
|
773
|
+
|
|
774
|
+
// Every byte value, so a text-mode body handler would visibly corrupt it.
|
|
775
|
+
const bytes = Buffer.from(Array.from({ length: 256 }, (_, i) => i));
|
|
776
|
+
const up = await requestUpload(
|
|
777
|
+
state.port, state.sessionSecret,
|
|
778
|
+
`/api/sessions/upload?name=${encodeURIComponent(session)}&filename=shot.png`,
|
|
779
|
+
bytes
|
|
780
|
+
);
|
|
781
|
+
assert.equal(up.status, 200);
|
|
782
|
+
assert.ok(up.data.path, 'returns the host path');
|
|
783
|
+
const landed = await fs.readFile(up.data.path);
|
|
784
|
+
assert.ok(landed.equals(bytes), 'bytes survive the round trip unmodified');
|
|
785
|
+
assert.equal(path.basename(up.data.path), 'shot.png');
|
|
786
|
+
assert.ok(up.data.path.startsWith(path.join(hopHome, 'uploads')),
|
|
787
|
+
`staged under HOP_HOME/uploads, got ${up.data.path}`);
|
|
788
|
+
|
|
789
|
+
// A second drop of the same name is a second file, not an overwrite.
|
|
790
|
+
const again = await requestUpload(
|
|
791
|
+
state.port, state.sessionSecret,
|
|
792
|
+
`/api/sessions/upload?name=${encodeURIComponent(session)}&filename=shot.png`,
|
|
793
|
+
Buffer.from('second')
|
|
794
|
+
);
|
|
795
|
+
assert.equal(again.status, 200);
|
|
796
|
+
assert.notEqual(again.data.path, up.data.path, 'collision gets its own path');
|
|
797
|
+
assert.ok((await fs.readFile(up.data.path)).equals(bytes), 'first upload untouched');
|
|
798
|
+
|
|
799
|
+
// Traversal in the filename must collapse to a basename inside staging.
|
|
800
|
+
const evil = await requestUpload(
|
|
801
|
+
state.port, state.sessionSecret,
|
|
802
|
+
`/api/sessions/upload?name=${encodeURIComponent(session)}&filename=${encodeURIComponent('../../../../etc/hop-pwned')}`,
|
|
803
|
+
Buffer.from('nope')
|
|
804
|
+
);
|
|
805
|
+
assert.equal(evil.status, 200);
|
|
806
|
+
assert.ok(evil.data.path.startsWith(path.join(hopHome, 'uploads')),
|
|
807
|
+
`traversal stayed in staging, got ${evil.data.path}`);
|
|
808
|
+
assert.equal(path.basename(evil.data.path), 'hop-pwned');
|
|
809
|
+
|
|
810
|
+
// An unknown session is not a place to write files.
|
|
811
|
+
const nowhere = await requestUpload(
|
|
812
|
+
state.port, state.sessionSecret,
|
|
813
|
+
'/api/sessions/upload?name=no-such-session-here&filename=x.txt',
|
|
814
|
+
Buffer.from('x')
|
|
815
|
+
);
|
|
816
|
+
assert.equal(nowhere.status, 404);
|
|
817
|
+
|
|
818
|
+
await requestJson(state.port, state.sessionSecret, 'POST', '/api/sessions/delete',
|
|
819
|
+
{ name: 'upload-probe', internalName: session });
|
|
820
|
+
});
|