Allow curl requests to any HTTPS host; remove experimental domain allowlist
Browse files- README.md +3 -3
- app/README.md +1 -1
- app/index.html +1 -1
- app/public/http-help.html +1 -1
- app/src/agent.worker.mjs +1 -1
- app/src/browser-http.mjs +4 -14
- app/src/workspace.mjs +2 -2
- app/tests/browser-http.test.mjs +7 -3
- app/tests/http-live.mjs +5 -3
- app/tests/protocol.test.mjs +2 -2
- assets/agent.worker-DONV7yi_.js +0 -0
- assets/index-BzGjruQ9.js +0 -0
- http-help.html +1 -1
- index.html +2 -2
- validation/REPORT.md +3 -1
README.md
CHANGED
|
@@ -34,9 +34,9 @@ jq '{id, title, completed}' todo.json
|
|
| 34 |
|
| 35 |
Or ask Pi: **Fetch https://jsonplaceholder.typicode.com/todos/1, save it as todo.json, and tell me its title and whether it is completed.**
|
| 36 |
|
| 37 |
-
|
| 38 |
|
| 39 |
-
Browser CORS applies. Redirects require the final URL directly: browsers hide redirect destinations, so this copy reports an error instead of silently returning empty output or following unchecked
|
| 40 |
|
| 41 |
Source is under `app/`. Rebuild with `cd app && npm ci && npm test && npm run build`, then `python3 scripts/stage.py`. The HTTP implementation is in `app/src/browser-http.mjs`; it uses just-bash's existing curl commands and custom transport hook. Tests and results are in `validation/`.
|
| 42 |
|
|
@@ -44,4 +44,4 @@ The small 4-bit model can make mistakes. Shell transport checks and actual-model
|
|
| 44 |
|
| 45 |
## Deployed results
|
| 46 |
|
| 47 |
-
The [evaluation report](validation/REPORT.md) separates 15 passing shell checks from seven real-model trials (five fully passed; two partial results). Basic API fetching works well. Duplicate POSTs and excessive retries on blocked requests remain model limitations.
|
|
|
|
| 34 |
|
| 35 |
Or ask Pi: **Fetch https://jsonplaceholder.typicode.com/todos/1, save it as todo.json, and tell me its title and whether it is completed.**
|
| 36 |
|
| 37 |
+
Any HTTPS URL is allowed, including GitHub Gists; there is no host allowlist. GET, HEAD, and POST are enabled, with an 8-second deadline and 1 MiB response limit. Use `set -o pipefail` before pipelines to retain download failures.
|
| 38 |
|
| 39 |
+
Browser CORS applies. Redirects require the final URL directly: browsers hide redirect destinations, so this copy reports an error instead of silently returning empty output or following unchecked redirect schemes. Cookies are omitted. Explicit request URLs, headers, and bodies are sent to the destination; inference and workspace storage remain local. Node.js, Python, npm, and native programs remain unavailable.
|
| 40 |
|
| 41 |
Source is under `app/`. Rebuild with `cd app && npm ci && npm test && npm run build`, then `python3 scripts/stage.py`. The HTTP implementation is in `app/src/browser-http.mjs`; it uses just-bash's existing curl commands and custom transport hook. Tests and results are in `validation/`.
|
| 42 |
|
|
|
|
| 44 |
|
| 45 |
## Deployed results
|
| 46 |
|
| 47 |
+
The initial, host-restricted [evaluation report](validation/REPORT.md) separates 15 passing shell checks from seven real-model trials (five fully passed; two partial results). Basic API fetching works well. Duplicate POSTs and excessive retries on blocked requests remain model limitations.
|
app/README.md
CHANGED
|
@@ -52,7 +52,7 @@ Message padding follows Pi 0.85.1 defaults: one column of output padding, one in
|
|
| 52 |
|
| 53 |
The full Node CLI, host filesystem, OAuth providers, extensions, package installation, and RPC server are not included. xterm.js is the terminal emulator; it does not add Node.js or a native shell. Model inference and tool execution remain in the browser worker. Terminal control sequences from model/file output are stripped before rendering, and Markdown images do not trigger downloads.
|
| 54 |
|
| 55 |
-
just-bash is a simulated shell, not a Linux VM. Shell scripts, pipes, redirects, `awk`, `jq`, `sed`, `grep`, `find`, and other supported text commands execute in JavaScript. Node.js, Python, npm, native executables, and SQLite are unavailable. This HTTP experiment enables curl for
|
| 56 |
|
| 57 |
Local model and filesystem tool calls can run with the browser network disabled after loading. HTTP commands require a connection and CORS permission from the destination. Reopening the app itself still requires its static HTML/JS/WASM assets; there is no offline service worker. Browser storage belongs to this site's origin and may be evicted by the browser. Export important work.
|
| 58 |
|
|
|
|
| 52 |
|
| 53 |
The full Node CLI, host filesystem, OAuth providers, extensions, package installation, and RPC server are not included. xterm.js is the terminal emulator; it does not add Node.js or a native shell. Model inference and tool execution remain in the browser worker. Terminal control sequences from model/file output are stripped before rendering, and Markdown images do not trigger downloads.
|
| 54 |
|
| 55 |
+
just-bash is a simulated shell, not a Linux VM. Shell scripts, pipes, redirects, `awk`, `jq`, `sed`, `grep`, `find`, and other supported text commands execute in JavaScript. Node.js, Python, npm, native executables, and SQLite are unavailable. This HTTP experiment enables curl for any HTTPS URL through a browser fetch adapter; see public/http-help.html for examples and limits. The installed browser bundle also excludes working gzip/zlib commands. Shell cwd and variables reset between commands; filesystem changes persist. Commands have a 10-second deadline, 3,000-command cap, 16 MiB filesystem budget, and 256 KiB output budget. Tool output passed to the model is capped at 6,000 characters.
|
| 56 |
|
| 57 |
Local model and filesystem tool calls can run with the browser network disabled after loading. HTTP commands require a connection and CORS permission from the destination. Reopening the app itself still requires its static HTML/JS/WASM assets; there is no offline service worker. Browser storage belongs to this site's origin and may be evicted by the browser. Export important work.
|
| 58 |
|
app/index.html
CHANGED
|
@@ -65,7 +65,7 @@
|
|
| 65 |
</form>
|
| 66 |
<div id="shell-hint"><span>Enter to run · Shift+Enter for newline</span><span>↑↓ history</span></div>
|
| 67 |
</details>
|
| 68 |
-
<p class="workspace-note">HTTP
|
| 69 |
</aside>
|
| 70 |
</main>
|
| 71 |
<dialog id="model-dialog" aria-labelledby="model-title" aria-describedby="model-description">
|
|
|
|
| 65 |
</form>
|
| 66 |
<div id="shell-hint"><span>Enter to run · Shift+Enter for newline</span><span>↑↓ history</span></div>
|
| 67 |
</details>
|
| 68 |
+
<p class="workspace-note">HTTP: curl can request any HTTPS URL. Browser CORS applies. <a href="./http-help.html" target="_blank" rel="noopener">HTTP help ↗</a></p>
|
| 69 |
</aside>
|
| 70 |
</main>
|
| 71 |
<dialog id="model-dialog" aria-labelledby="model-title" aria-describedby="model-description">
|
app/public/http-help.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
<style>html{color-scheme:dark;background:#18181e;color:#e4e4eb;font:17px/1.6 system-ui}body{max-width:760px;margin:40px auto;padding:0 20px}a{color:#a4e2d4}pre{background:#262630;padding:16px;overflow:auto;border-radius:12px}code{font-size:14px}</style>
|
| 5 |
<h1>HTTP from the browser shell</h1>
|
| 6 |
<p>This experimental copy enables real <code>curl</code> requests. The model and shell run on your device. Requests go directly from your browser to the destination; there is no proxy. Request URLs, headers, and bodies are sent to that service.</p>
|
| 7 |
-
<p>
|
| 8 |
<h2>Try in Shell</h2>
|
| 9 |
<pre>curl -fsS https://jsonplaceholder.typicode.com/todos/1 -o todo.json
|
| 10 |
jq '{id, title, completed}' todo.json</pre>
|
|
|
|
| 4 |
<style>html{color-scheme:dark;background:#18181e;color:#e4e4eb;font:17px/1.6 system-ui}body{max-width:760px;margin:40px auto;padding:0 20px}a{color:#a4e2d4}pre{background:#262630;padding:16px;overflow:auto;border-radius:12px}code{font-size:14px}</style>
|
| 5 |
<h1>HTTP from the browser shell</h1>
|
| 6 |
<p>This experimental copy enables real <code>curl</code> requests. The model and shell run on your device. Requests go directly from your browser to the destination; there is no proxy. Request URLs, headers, and bodies are sent to that service.</p>
|
| 7 |
+
<p>Any HTTPS URL is allowed, including GitHub Gists; there is no host allowlist. The destination must still permit browser access with CORS. GET, HEAD, and POST are enabled. Cookies are omitted. Requests have an 8-second deadline and a 1 MiB response limit.</p>
|
| 8 |
<h2>Try in Shell</h2>
|
| 9 |
<pre>curl -fsS https://jsonplaceholder.typicode.com/todos/1 -o todo.json
|
| 10 |
jq '{id, title, completed}' todo.json</pre>
|
app/src/agent.worker.mjs
CHANGED
|
@@ -14,7 +14,7 @@ When the user explicitly asks you to use a tool, call that tool even if you alre
|
|
| 14 |
Working directory: /workspace. Use read, write, edit, and bash. Keep tool calls and answers concise.
|
| 15 |
The bash tool supports cat, ls, find, grep, sed, awk, jq, sort, wc, printf, pipes, redirects, and shell scripts.
|
| 16 |
Use jq for JSON and awk for CSV and calculations; python, python3, node, npm, and native binaries are unavailable.
|
| 17 |
-
Use curl -fsS for
|
| 18 |
You can create and edit text files, including HTML, CSS, JavaScript, and shell scripts. External URLs may be referenced without fetching them. Only claim an asset was fetched or tested after a successful tool result. HTTP requests send the requested URL and any explicit request data to that external server; model inference stays local.
|
| 19 |
Interpret short follow-ups using the conversation and inspect the relevant files when needed.
|
| 20 |
Inspect files before editing them. After changing files, verify the result with a tool. Finish with a short factual answer.`;
|
|
|
|
| 14 |
Working directory: /workspace. Use read, write, edit, and bash. Keep tool calls and answers concise.
|
| 15 |
The bash tool supports cat, ls, find, grep, sed, awk, jq, sort, wc, printf, pipes, redirects, and shell scripts.
|
| 16 |
Use jq for JSON and awk for CSV and calculations; python, python3, node, npm, and native binaries are unavailable.
|
| 17 |
+
Use curl -fsS for requests to any HTTPS URL; there is no host allowlist. To save a response, use curl -fsS URL -o file.json, then inspect it with jq or read. For pipelines use set -o pipefail so failed downloads are not mistaken for success. CORS and redirect errors are browser limitations: report them accurately; do not invent fetched content or repeatedly retry blocked requests.
|
| 18 |
You can create and edit text files, including HTML, CSS, JavaScript, and shell scripts. External URLs may be referenced without fetching them. Only claim an asset was fetched or tested after a successful tool result. HTTP requests send the requested URL and any explicit request data to that external server; model inference stays local.
|
| 19 |
Interpret short follow-ups using the conversation and inspect the relevant files when needed.
|
| 20 |
Inspect files before editing them. After changing files, verify the result with a tool. Finish with a short factual answer.`;
|
app/src/browser-http.mjs
CHANGED
|
@@ -1,23 +1,13 @@
|
|
| 1 |
-
// just-bash's curl
|
| 2 |
-
export const HTTP_ORIGINS = Object.freeze([
|
| 3 |
-
'https://api.github.com',
|
| 4 |
-
'https://raw.githubusercontent.com',
|
| 5 |
-
'https://huggingface.co',
|
| 6 |
-
'https://jsonplaceholder.typicode.com',
|
| 7 |
-
'https://httpbin.org',
|
| 8 |
-
]);
|
| 9 |
export const HTTP_METHODS = Object.freeze(['GET', 'HEAD', 'POST']);
|
| 10 |
export const HTTP_LIMIT = 1024 * 1024;
|
| 11 |
export const HTTP_TIMEOUT = 8_000;
|
| 12 |
|
| 13 |
-
export function createBrowserFetch({ transport = globalThis.fetch,
|
| 14 |
timeoutMs = HTTP_TIMEOUT, maxBytes = HTTP_LIMIT } = {}) {
|
| 15 |
-
const allowed = new Set(origins);
|
| 16 |
return async (input, options = {}) => {
|
| 17 |
const url = new URL(input);
|
| 18 |
-
if (url.protocol !== 'https:'
|
| 19 |
-
throw Error(`HTTP destination is not enabled in this experiment. Allowed hosts: ${[...allowed].map(x => new URL(x).host).join(', ')}.`);
|
| 20 |
-
}
|
| 21 |
if (url.username || url.password) throw Error('Credentials in URLs are unsupported. Use an explicit Authorization header if needed.');
|
| 22 |
const method = (options.method ?? 'GET').toUpperCase();
|
| 23 |
if (!HTTP_METHODS.includes(method)) throw Error(`HTTP method ${method} is not enabled. Use GET, HEAD, or POST.`);
|
|
@@ -33,7 +23,7 @@ export function createBrowserFetch({ transport = globalThis.fetch, origins = HTT
|
|
| 33 |
body: ['GET', 'HEAD'].includes(method) ? undefined : options.body,
|
| 34 |
mode: 'cors', credentials: 'omit', redirect: 'manual', referrerPolicy: 'no-referrer', signal });
|
| 35 |
// Browsers hide both the Location header and status on manual redirects.
|
| 36 |
-
// Following automatically would skip the
|
| 37 |
if (response.type === 'opaqueredirect' || (response.status >= 300 && response.status < 400 && response.status !== 304)) {
|
| 38 |
throw Error('HTTP redirect cannot be followed safely in this browser shell. Use the final HTTPS URL directly (including exact capitalization).');
|
| 39 |
}
|
|
|
|
| 1 |
+
// just-bash's curl uses this browser transport. There is no proxy/server.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
export const HTTP_METHODS = Object.freeze(['GET', 'HEAD', 'POST']);
|
| 3 |
export const HTTP_LIMIT = 1024 * 1024;
|
| 4 |
export const HTTP_TIMEOUT = 8_000;
|
| 5 |
|
| 6 |
+
export function createBrowserFetch({ transport = globalThis.fetch,
|
| 7 |
timeoutMs = HTTP_TIMEOUT, maxBytes = HTTP_LIMIT } = {}) {
|
|
|
|
| 8 |
return async (input, options = {}) => {
|
| 9 |
const url = new URL(input);
|
| 10 |
+
if (url.protocol !== 'https:') throw Error('Only HTTPS URLs are supported. Use https:// instead.');
|
|
|
|
|
|
|
| 11 |
if (url.username || url.password) throw Error('Credentials in URLs are unsupported. Use an explicit Authorization header if needed.');
|
| 12 |
const method = (options.method ?? 'GET').toUpperCase();
|
| 13 |
if (!HTTP_METHODS.includes(method)) throw Error(`HTTP method ${method} is not enabled. Use GET, HEAD, or POST.`);
|
|
|
|
| 23 |
body: ['GET', 'HEAD'].includes(method) ? undefined : options.body,
|
| 24 |
mode: 'cors', credentials: 'omit', redirect: 'manual', referrerPolicy: 'no-referrer', signal });
|
| 25 |
// Browsers hide both the Location header and status on manual redirects.
|
| 26 |
+
// Following automatically would skip the HTTPS check at intermediate hops.
|
| 27 |
if (response.type === 'opaqueredirect' || (response.status >= 300 && response.status < 400 && response.status !== 304)) {
|
| 28 |
throw Error('HTTP redirect cannot be followed safely in this browser shell. Use the final HTTPS URL directly (including exact capitalization).');
|
| 29 |
}
|
app/src/workspace.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import { Bash } from 'just-bash/browser';
|
| 2 |
import { Type } from '@earendil-works/pi-ai';
|
| 3 |
-
import { createBrowserFetch
|
| 4 |
|
| 5 |
// Frozen previous starter data, used only to recognize an unmodified demo.
|
| 6 |
export const legacyProductsCsv = 'product,unit_cost,stock\nNotebook,2.50,24\nPencil,0.50,120\nFolder,1.20,18\nEraser,0.35,40\nStapler,2.75,9\nRuler,0.80,30\n';
|
|
@@ -52,7 +52,7 @@ export function createWorkspace(files = initialFiles, entries) {
|
|
| 52 |
await bash.fs.writeFile(resolved, content);
|
| 53 |
}
|
| 54 |
const tools = [{
|
| 55 |
-
name: 'bash', label: 'Bash', description:
|
| 56 |
parameters: Type.Object({ command: Type.String({ description: 'The shell command to execute.' }) }),
|
| 57 |
execute: async (_id, { command }, signal) => {
|
| 58 |
const r = await bash.exec(command, { signal });
|
|
|
|
| 1 |
import { Bash } from 'just-bash/browser';
|
| 2 |
import { Type } from '@earendil-works/pi-ai';
|
| 3 |
+
import { createBrowserFetch } from './browser-http.mjs';
|
| 4 |
|
| 5 |
// Frozen previous starter data, used only to recognize an unmodified demo.
|
| 6 |
export const legacyProductsCsv = 'product,unit_cost,stock\nNotebook,2.50,24\nPencil,0.50,120\nFolder,1.20,18\nEraser,0.35,40\nStapler,2.75,9\nRuler,0.80,30\n';
|
|
|
|
| 52 |
await bash.fs.writeFile(resolved, content);
|
| 53 |
}
|
| 54 |
const tools = [{
|
| 55 |
+
name: 'bash', label: 'Bash', description: 'Run a bash command in /workspace. Supports cat, ls, grep, sed, awk, jq, sort, pipes, redirects, shell scripts, and curl. HTTP GET/HEAD/POST is enabled for any HTTPS URL; there is no host allowlist. Use curl -fsS URL; use set -o pipefail before piping HTTP output to jq. Browser CORS applies; redirects require the final URL. HTTP limits: 8 seconds, 1 MiB. No Node.js, Python, npm, or native binaries. Files persist across commands; cd and variables reset each call.',
|
| 56 |
parameters: Type.Object({ command: Type.String({ description: 'The shell command to execute.' }) }),
|
| 57 |
execute: async (_id, { command }, signal) => {
|
| 58 |
const r = await bash.exec(command, { signal });
|
app/tests/browser-http.test.mjs
CHANGED
|
@@ -15,15 +15,19 @@ test('HTTP preserves bytes/status and omits implicit credentials', async () => {
|
|
| 15 |
assert.equal(request.init.credentials, 'omit'); assert.equal(request.init.redirect, 'manual');
|
| 16 |
assert.equal(request.init.body, 'hello'); assert.equal(request.init.mode, 'cors');
|
| 17 |
});
|
| 18 |
-
test('
|
| 19 |
let calls = 0;
|
| 20 |
const fetch = createBrowserFetch({ transport: async () => { calls++; return new Response(''); } });
|
| 21 |
-
for (const url of ['http://httpbin.org/get', '
|
| 22 |
-
await assert.rejects(fetch(url), /
|
| 23 |
}
|
| 24 |
await assert.rejects(fetch('https://user:pass@httpbin.org/get'), /Credentials/);
|
| 25 |
await assert.rejects(fetch(endpoint, { method: 'DELETE' }), /method DELETE/);
|
| 26 |
assert.equal(calls, 0);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
});
|
| 28 |
test('Opaque redirects cannot appear as successful empty responses', async () => {
|
| 29 |
const fetch = createBrowserFetch({ transport: async () => ({ type: 'opaqueredirect', status: 0 }) });
|
|
|
|
| 15 |
assert.equal(request.init.credentials, 'omit'); assert.equal(request.init.redirect, 'manual');
|
| 16 |
assert.equal(request.init.body, 'hello'); assert.equal(request.init.mode, 'cors');
|
| 17 |
});
|
| 18 |
+
test('Any HTTPS host is accepted; protocol/method/URL credentials are checked before requests', async () => {
|
| 19 |
let calls = 0;
|
| 20 |
const fetch = createBrowserFetch({ transport: async () => { calls++; return new Response(''); } });
|
| 21 |
+
for (const url of ['http://httpbin.org/get', 'ftp://example.com/file', 'file:///etc/passwd', 'data:text/plain,hello', 'http://127.0.0.1/']) {
|
| 22 |
+
await assert.rejects(fetch(url), /Only HTTPS/);
|
| 23 |
}
|
| 24 |
await assert.rejects(fetch('https://user:pass@httpbin.org/get'), /Credentials/);
|
| 25 |
await assert.rejects(fetch(endpoint, { method: 'DELETE' }), /method DELETE/);
|
| 26 |
assert.equal(calls, 0);
|
| 27 |
+
for (const url of ['https://example.com/', 'https://gist.github.com/', 'https://gist.githubusercontent.com/', 'https://another-host.test:8443/data']) {
|
| 28 |
+
assert.equal((await fetch(url)).status, 200);
|
| 29 |
+
}
|
| 30 |
+
assert.equal(calls, 4);
|
| 31 |
});
|
| 32 |
test('Opaque redirects cannot appear as successful empty responses', async () => {
|
| 33 |
const fetch = createBrowserFetch({ transport: async () => ({ type: 'opaqueredirect', status: 0 }) });
|
app/tests/http-live.mjs
CHANGED
|
@@ -16,6 +16,8 @@ context.on('request', r => { if (!r.url().startsWith(new URL(url).origin)) resul
|
|
| 16 |
const rpc = (action, args) => page.evaluate(({ action, args }) => window.browserPi.rpc(action, args), { action, args });
|
| 17 |
const save = () => fs.writeFile(root + '/validation/http-shell-live.json', JSON.stringify(result, null, 2) + '\n');
|
| 18 |
const cases = [
|
|
|
|
|
|
|
| 19 |
['json-get-save', 'curl -fsS https://jsonplaceholder.typicode.com/todos/1 -o todo.json && jq -r .title todo.json', r => r.exitCode === 0 && r.stdout.includes('delectus aut autem')],
|
| 20 |
['github-pipeline', "set -o pipefail; curl -fsS https://api.github.com/repos/huggingface/transformers.js | jq -r '.full_name, .license.spdx_id'", r => r.exitCode === 0 && r.stdout.includes('huggingface/transformers.js')],
|
| 21 |
['hf-metadata', 'set -o pipefail; curl -fsS https://huggingface.co/api/models/openbmb/MiniCPM5-2B | jq -r .id', r => r.exitCode === 0 && /openbmb\/MiniCPM5-2B/i.test(r.stdout)],
|
|
@@ -24,15 +26,15 @@ const cases = [
|
|
| 24 |
['post-json', `set -o pipefail; curl -fsS https://httpbin.org/post -H 'Content-Type: application/json' -d '{"demo":"browser-http","value":144}' | jq -c .json`, r => r.exitCode === 0 && r.stdout.includes('browser-http') && r.stdout.includes('144')],
|
| 25 |
['request-headers', "set -o pipefail; curl -fsS https://httpbin.org/headers -H 'X-Demo: browser-http' | jq -r '.headers[\"X-Demo\"]'", r => r.exitCode === 0 && r.stdout.trim() === 'browser-http'],
|
| 26 |
['http-404', 'curl -fsS https://jsonplaceholder.typicode.com/does-not-exist', r => r.exitCode !== 0 && /404/.test(r.stderr)],
|
| 27 |
-
['
|
| 28 |
['cors-failure', 'curl -fsS https://huggingface.co/', r => r.exitCode !== 0 && /CORS/.test(r.stderr)],
|
| 29 |
['redirect-failure', 'curl -fsSL https://httpbin.org/redirect-to?url=https%3A%2F%2Fhttpbin.org%2Fget', r => r.exitCode !== 0 && /redirect/i.test(r.stderr)],
|
| 30 |
['timeout', 'curl -fsS --max-time 1 https://httpbin.org/delay/5', r => r.exitCode !== 0 && /timed out/i.test(r.stderr)],
|
| 31 |
-
['failed-pipeline', 'set -o pipefail; curl -fsS
|
| 32 |
];
|
| 33 |
try {
|
| 34 |
await page.goto(url); await page.waitForFunction(() => window.browserPi?.terminal); await page.evaluate(() => window.browserPi.ready);
|
| 35 |
-
assert.match(await page.locator('.workspace-note').textContent(), /
|
| 36 |
for (const [name, command, check] of cases) {
|
| 37 |
const start = Date.now(); const response = await rpc('shell', { command });
|
| 38 |
const entry = { name, command, elapsedMs: Date.now() - start, stdout: response.stdout, stderr: response.stderr, exitCode: response.exitCode, passed: check(response) };
|
|
|
|
| 16 |
const rpc = (action, args) => page.evaluate(({ action, args }) => window.browserPi.rpc(action, args), { action, args });
|
| 17 |
const save = () => fs.writeFile(root + '/validation/http-shell-live.json', JSON.stringify(result, null, 2) + '\n');
|
| 18 |
const cases = [
|
| 19 |
+
['new-host-json', 'curl -fsS https://dummyjson.com/products/1 -o product.json && jq -r .id product.json', r => r.exitCode === 0 && r.stdout.trim() === '1'],
|
| 20 |
+
['public-gist', 'curl -fsS https://gist.githubusercontent.com/mbostock/4063269/raw/b111d979d7fcd0fabb554cccd37caf2691f31706/flare.csv -o gist.csv && head -n 1 gist.csv', r => r.exitCode === 0 && r.stdout.trim() === 'id,value'],
|
| 21 |
['json-get-save', 'curl -fsS https://jsonplaceholder.typicode.com/todos/1 -o todo.json && jq -r .title todo.json', r => r.exitCode === 0 && r.stdout.includes('delectus aut autem')],
|
| 22 |
['github-pipeline', "set -o pipefail; curl -fsS https://api.github.com/repos/huggingface/transformers.js | jq -r '.full_name, .license.spdx_id'", r => r.exitCode === 0 && r.stdout.includes('huggingface/transformers.js')],
|
| 23 |
['hf-metadata', 'set -o pipefail; curl -fsS https://huggingface.co/api/models/openbmb/MiniCPM5-2B | jq -r .id', r => r.exitCode === 0 && /openbmb\/MiniCPM5-2B/i.test(r.stdout)],
|
|
|
|
| 26 |
['post-json', `set -o pipefail; curl -fsS https://httpbin.org/post -H 'Content-Type: application/json' -d '{"demo":"browser-http","value":144}' | jq -c .json`, r => r.exitCode === 0 && r.stdout.includes('browser-http') && r.stdout.includes('144')],
|
| 27 |
['request-headers', "set -o pipefail; curl -fsS https://httpbin.org/headers -H 'X-Demo: browser-http' | jq -r '.headers[\"X-Demo\"]'", r => r.exitCode === 0 && r.stdout.trim() === 'browser-http'],
|
| 28 |
['http-404', 'curl -fsS https://jsonplaceholder.typicode.com/does-not-exist', r => r.exitCode !== 0 && /404/.test(r.stderr)],
|
| 29 |
+
['http-rejected', 'curl -fsS http://example.com/', r => r.exitCode !== 0 && /Only HTTPS/.test(r.stderr)],
|
| 30 |
['cors-failure', 'curl -fsS https://huggingface.co/', r => r.exitCode !== 0 && /CORS/.test(r.stderr)],
|
| 31 |
['redirect-failure', 'curl -fsSL https://httpbin.org/redirect-to?url=https%3A%2F%2Fhttpbin.org%2Fget', r => r.exitCode !== 0 && /redirect/i.test(r.stderr)],
|
| 32 |
['timeout', 'curl -fsS --max-time 1 https://httpbin.org/delay/5', r => r.exitCode !== 0 && /timed out/i.test(r.stderr)],
|
| 33 |
+
['failed-pipeline', 'set -o pipefail; curl -fsS http://example.com/ | jq .', r => r.exitCode !== 0],
|
| 34 |
];
|
| 35 |
try {
|
| 36 |
await page.goto(url); await page.waitForFunction(() => window.browserPi?.terminal); await page.evaluate(() => window.browserPi.ready);
|
| 37 |
+
assert.match(await page.locator('.workspace-note').textContent(), /any HTTPS URL/);
|
| 38 |
for (const [name, command, check] of cases) {
|
| 39 |
const start = Date.now(); const response = await rpc('shell', { command });
|
| 40 |
const entry = { name, command, elapsedMs: Date.now() - start, stdout: response.stdout, stderr: response.stderr, exitCode: response.exitCode, passed: check(response) };
|
app/tests/protocol.test.mjs
CHANGED
|
@@ -113,8 +113,8 @@ test('real just-bash executes pipelines and scripts; filesystem survives reload
|
|
| 113 |
await workspace.tools.find(t => t.name === 'edit').execute('2', { path: 'hello.sh', oldText: 'Hello', newText: '$& literal' });
|
| 114 |
assert.match(await workspace.read('hello.sh'), /\$& literal/);
|
| 115 |
const restored = createWorkspace(await workspace.snapshot()); assert.equal(await restored.read('total.txt'), '144\n');
|
| 116 |
-
const denied = await workspace.bash.exec('curl
|
| 117 |
-
assert.notEqual(denied.exitCode, 0); assert.match(denied.stderr, /
|
| 118 |
assert.equal((await workspace.bash.exec('node -v')).exitCode, 127);
|
| 119 |
});
|
| 120 |
test('workspace persistence preserves executable modes, empty directories, symlinks and bytes', async () => {
|
|
|
|
| 113 |
await workspace.tools.find(t => t.name === 'edit').execute('2', { path: 'hello.sh', oldText: 'Hello', newText: '$& literal' });
|
| 114 |
assert.match(await workspace.read('hello.sh'), /\$& literal/);
|
| 115 |
const restored = createWorkspace(await workspace.snapshot()); assert.equal(await restored.read('total.txt'), '144\n');
|
| 116 |
+
const denied = await workspace.bash.exec('curl http://example.com');
|
| 117 |
+
assert.notEqual(denied.exitCode, 0); assert.match(denied.stderr, /Only HTTPS/);
|
| 118 |
assert.equal((await workspace.bash.exec('node -v')).exitCode, 127);
|
| 119 |
});
|
| 120 |
test('workspace persistence preserves executable modes, empty directories, symlinks and bytes', async () => {
|
assets/agent.worker-DONV7yi_.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
assets/index-BzGjruQ9.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
http-help.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
<style>html{color-scheme:dark;background:#18181e;color:#e4e4eb;font:17px/1.6 system-ui}body{max-width:760px;margin:40px auto;padding:0 20px}a{color:#a4e2d4}pre{background:#262630;padding:16px;overflow:auto;border-radius:12px}code{font-size:14px}</style>
|
| 5 |
<h1>HTTP from the browser shell</h1>
|
| 6 |
<p>This experimental copy enables real <code>curl</code> requests. The model and shell run on your device. Requests go directly from your browser to the destination; there is no proxy. Request URLs, headers, and bodies are sent to that service.</p>
|
| 7 |
-
<p>
|
| 8 |
<h2>Try in Shell</h2>
|
| 9 |
<pre>curl -fsS https://jsonplaceholder.typicode.com/todos/1 -o todo.json
|
| 10 |
jq '{id, title, completed}' todo.json</pre>
|
|
|
|
| 4 |
<style>html{color-scheme:dark;background:#18181e;color:#e4e4eb;font:17px/1.6 system-ui}body{max-width:760px;margin:40px auto;padding:0 20px}a{color:#a4e2d4}pre{background:#262630;padding:16px;overflow:auto;border-radius:12px}code{font-size:14px}</style>
|
| 5 |
<h1>HTTP from the browser shell</h1>
|
| 6 |
<p>This experimental copy enables real <code>curl</code> requests. The model and shell run on your device. Requests go directly from your browser to the destination; there is no proxy. Request URLs, headers, and bodies are sent to that service.</p>
|
| 7 |
+
<p>Any HTTPS URL is allowed, including GitHub Gists; there is no host allowlist. The destination must still permit browser access with CORS. GET, HEAD, and POST are enabled. Cookies are omitted. Requests have an 8-second deadline and a 1 MiB response limit.</p>
|
| 8 |
<h2>Try in Shell</h2>
|
| 9 |
<pre>curl -fsS https://jsonplaceholder.typicode.com/todos/1 -o todo.json
|
| 10 |
jq '{id, title, completed}' todo.json</pre>
|
index.html
CHANGED
|
@@ -21,7 +21,7 @@
|
|
| 21 |
<meta name="twitter:image" content="https://huggingface.co/spaces/Mike0021/MiniCPM5-2B-WebGPU-Pi-HTTP/resolve/main/social-thumbnail-v3.png">
|
| 22 |
<meta name="twitter:image:alt" content="A coding agent running entirely in your browser, above the Pi terminal and its pixel cat mascot.">
|
| 23 |
<title>Pi · Coding agent in your browser</title>
|
| 24 |
-
<script type="module" crossorigin src="./assets/index-
|
| 25 |
<link rel="stylesheet" crossorigin href="./assets/index-CLisF1Ml.css">
|
| 26 |
</head>
|
| 27 |
<body>
|
|
@@ -67,7 +67,7 @@
|
|
| 67 |
</form>
|
| 68 |
<div id="shell-hint"><span>Enter to run · Shift+Enter for newline</span><span>↑↓ history</span></div>
|
| 69 |
</details>
|
| 70 |
-
<p class="workspace-note">HTTP
|
| 71 |
</aside>
|
| 72 |
</main>
|
| 73 |
<dialog id="model-dialog" aria-labelledby="model-title" aria-describedby="model-description">
|
|
|
|
| 21 |
<meta name="twitter:image" content="https://huggingface.co/spaces/Mike0021/MiniCPM5-2B-WebGPU-Pi-HTTP/resolve/main/social-thumbnail-v3.png">
|
| 22 |
<meta name="twitter:image:alt" content="A coding agent running entirely in your browser, above the Pi terminal and its pixel cat mascot.">
|
| 23 |
<title>Pi · Coding agent in your browser</title>
|
| 24 |
+
<script type="module" crossorigin src="./assets/index-BzGjruQ9.js"></script>
|
| 25 |
<link rel="stylesheet" crossorigin href="./assets/index-CLisF1Ml.css">
|
| 26 |
</head>
|
| 27 |
<body>
|
|
|
|
| 67 |
</form>
|
| 68 |
<div id="shell-hint"><span>Enter to run · Shift+Enter for newline</span><span>↑↓ history</span></div>
|
| 69 |
</details>
|
| 70 |
+
<p class="workspace-note">HTTP: curl can request any HTTPS URL. Browser CORS applies. <a href="./http-help.html" target="_blank" rel="noopener">HTTP help ↗</a></p>
|
| 71 |
</aside>
|
| 72 |
</main>
|
| 73 |
<dialog id="model-dialog" aria-labelledby="model-title" aria-describedby="model-description">
|
validation/REPORT.md
CHANGED
|
@@ -1,4 +1,6 @@
|
|
| 1 |
-
# Browser HTTP experiment —
|
|
|
|
|
|
|
| 2 |
|
| 3 |
Space: https://huggingface.co/spaces/Mike0021/MiniCPM5-2B-WebGPU-Pi-HTTP
|
| 4 |
|
|
|
|
| 1 |
+
# Browser HTTP experiment — initial host-restricted evaluation
|
| 2 |
+
|
| 3 |
+
> Historical results: the five-host restriction tested below has since been removed. The current app accepts any HTTPS URL, subject to browser CORS and the existing request limits.
|
| 4 |
|
| 5 |
Space: https://huggingface.co/spaces/Mike0021/MiniCPM5-2B-WebGPU-Pi-HTTP
|
| 6 |
|