svamp-cli 0.2.60 → 0.2.65

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.
@@ -54,17 +54,20 @@ async function handleServeCommand() {
54
54
  }
55
55
  }
56
56
  async function serveAdd(args, machineId) {
57
- const { connectAndGetMachine } = await import('./commands-B0kLKYmc.mjs');
57
+ const { connectAndGetMachine } = await import('./commands-CgirOjun.mjs');
58
58
  const pos = positionalArgs(args);
59
59
  const name = pos[0];
60
60
  if (!name) {
61
- console.error("Usage: svamp serve [add] <name> [directory] [--public | --access email1,email2]");
61
+ console.error("Usage: svamp serve [add] <name> [directory] [--public | --owner | --access email1,email2]");
62
+ console.error(" Default: capability URL (link mode) \u2014 anyone with the URL can view, no login required.");
62
63
  process.exit(1);
63
64
  }
64
65
  const directory = path.resolve(pos[1] || ".");
65
- let access = "owner";
66
+ let access = "link";
66
67
  if (hasFlag(args, "--public")) {
67
68
  access = "public";
69
+ } else if (hasFlag(args, "--owner")) {
70
+ access = "owner";
68
71
  } else {
69
72
  const accessFlag = getFlag(args, "--access");
70
73
  if (accessFlag) {
@@ -75,8 +78,12 @@ async function serveAdd(args, machineId) {
75
78
  try {
76
79
  const result = await machine.serveAdd({ name, directory, access });
77
80
  console.log(`Mount added: ${name} \u2192 ${directory}`);
78
- console.log(`Access: ${access === "public" ? "public" : access === "owner" ? "owner only" : access.join(", ")}`);
81
+ const accessLabel = access === "public" ? "public (no auth)" : access === "link" ? "link (capability URL \u2014 anyone with the URL can view)" : access === "owner" ? "owner only (Hypha login)" : `${access.join(", ")} (Hypha login)`;
82
+ console.log(`Access: ${accessLabel}`);
79
83
  console.log(`URL: ${result.url}`);
84
+ if (access === "link") {
85
+ console.log(`(URL embeds a long capability token; keep it secret to keep the mount private.)`);
86
+ }
80
87
  } catch (err) {
81
88
  console.error(`Error: ${err.message || err}`);
82
89
  process.exit(1);
@@ -86,7 +93,7 @@ async function serveAdd(args, machineId) {
86
93
  }
87
94
  }
88
95
  async function serveApply(args, machineId) {
89
- const { connectAndGetMachine } = await import('./commands-B0kLKYmc.mjs');
96
+ const { connectAndGetMachine } = await import('./commands-CgirOjun.mjs');
90
97
  const fs = await import('fs');
91
98
  const yaml = await import('yaml');
92
99
  const file = positionalArgs(args)[0];
@@ -107,7 +114,10 @@ async function serveApply(args, machineId) {
107
114
  console.error(" warmup_timeout_ms: 30000");
108
115
  console.error(" idle_timeout_sec: 600 # 0 = always running");
109
116
  console.error(" wake_on_request: true # spawn lazily on first request");
110
- console.error(" access: public # public | owner (default) | [emails]");
117
+ console.error(" access: link # link (default) | public | owner | [emails]");
118
+ console.error(" # link \u2014 capability URL, anyone with the URL can view");
119
+ console.error(" # public \u2014 fully open, short URL");
120
+ console.error(" # owner / [emails] \u2014 Hypha login required");
111
121
  process.exit(1);
112
122
  }
113
123
  if (!fs.existsSync(file)) {
@@ -150,7 +160,8 @@ async function serveApply(args, machineId) {
150
160
  directory: parsed.directory ? path.resolve(parsed.directory) : void 0,
151
161
  process: proc,
152
162
  sessionId: parsed.sessionId ?? parsed.session_id,
153
- access: parsed.access ?? "owner",
163
+ access: parsed.access ?? "link",
164
+ // default flipped from 'owner' to 'link' (capability URL)
154
165
  ownerEmail: parsed.ownerEmail ?? parsed.owner_email
155
166
  };
156
167
  const { machine, server } = await connectAndGetMachine(machineId);
@@ -171,7 +182,7 @@ async function serveApply(args, machineId) {
171
182
  }
172
183
  }
173
184
  async function serveRemove(args, machineId) {
174
- const { connectAndGetMachine } = await import('./commands-B0kLKYmc.mjs');
185
+ const { connectAndGetMachine } = await import('./commands-CgirOjun.mjs');
175
186
  const pos = positionalArgs(args);
176
187
  const name = pos[0];
177
188
  if (!name) {
@@ -191,7 +202,7 @@ async function serveRemove(args, machineId) {
191
202
  }
192
203
  }
193
204
  async function serveList(args, machineId) {
194
- const { connectAndGetMachine } = await import('./commands-B0kLKYmc.mjs');
205
+ const { connectAndGetMachine } = await import('./commands-CgirOjun.mjs');
195
206
  const all = hasFlag(args, "--all", "-a");
196
207
  const json = hasFlag(args, "--json");
197
208
  const sessionId = getFlag(args, "--session");
@@ -224,7 +235,7 @@ async function serveList(args, machineId) {
224
235
  }
225
236
  }
226
237
  async function serveInfo(machineId) {
227
- const { connectAndGetMachine } = await import('./commands-B0kLKYmc.mjs');
238
+ const { connectAndGetMachine } = await import('./commands-CgirOjun.mjs');
228
239
  const { machine, server } = await connectAndGetMachine(machineId);
229
240
  try {
230
241
  const info = await machine.serveInfo();
@@ -258,10 +269,14 @@ Usage:
258
269
  svamp serve list [--all] [--json] List mounts (default: current session only)
259
270
  svamp serve info Show server status and URL
260
271
 
261
- Access control (default: owner only):
262
- --public Allow anyone to access (no auth)
263
- --access email1,email2 Allow specific users (comma-separated emails)
264
- (no flag) Owner only \u2014 requires Hypha login
272
+ Access tiers (default: link):
273
+ (no flag) Capability URL \u2014 anyone with the URL can view.
274
+ URL embeds a long random token (~192-bit entropy)
275
+ as the first path segment. No login required.
276
+ Best for embedding in agent artifact iframes.
277
+ --public Fully public \u2014 no auth, short URL.
278
+ --owner Hypha login required, owner-only.
279
+ --access email1,email2 Hypha login required, specific allowed emails.
265
280
 
266
281
  Options:
267
282
  -m, --machine <id> Target a specific machine
@@ -270,12 +285,13 @@ Options:
270
285
  --json Output as JSON
271
286
 
272
287
  Examples:
273
- svamp serve my-report ./output # Owner-only (default)
274
- svamp serve dashboard ./dist --public # Anyone can access
275
- svamp serve data ./csv --access a@x.com,b@y.com # Specific users
276
- svamp serve apply my-app.yaml # Declarative apply
277
- svamp serve list --all # Show all mounts
278
- svamp serve remove my-report # Stop serving
288
+ svamp serve my-report ./output # Default: capability URL
289
+ svamp serve dashboard ./dist --public # Anyone, short URL
290
+ svamp serve data ./csv --owner # Hypha login required
291
+ svamp serve data ./csv --access a@x.com,b@y.com # Specific Hypha users
292
+ svamp serve apply my-app.yaml # Declarative apply
293
+ svamp serve list --all # Show all mounts
294
+ svamp serve remove my-report # Stop serving
279
295
 
280
296
  Declarative apply (svamp serve apply <yaml>):
281
297
  name: my-app
@@ -1,14 +1,15 @@
1
1
  import { spawn } from 'child_process';
2
+ import * as crypto from 'crypto';
2
3
  import * as fs from 'fs';
3
4
  import * as http from 'http';
4
5
  import * as net from 'net';
5
6
  import * as path from 'path';
6
- import { S as ServeAuth, h as hasCookieToken } from './run-Dhliie9Z.mjs';
7
+ import { S as ServeAuth, h as hasCookieToken } from './run-EPzdDXeY.mjs';
7
8
  import 'os';
8
9
  import 'fs/promises';
9
10
  import 'url';
10
- import 'crypto';
11
11
  import 'node:fs';
12
+ import 'util';
12
13
  import 'node:crypto';
13
14
  import 'node:path';
14
15
  import 'node:child_process';
@@ -21,6 +22,9 @@ import 'zod';
21
22
  import 'node:fs/promises';
22
23
  import 'node:util';
23
24
 
25
+ function generateLinkToken() {
26
+ return crypto.randomBytes(16).toString("hex");
27
+ }
24
28
  function findFreePort() {
25
29
  return new Promise((resolve, reject) => {
26
30
  const srv = net.createServer();
@@ -88,7 +92,7 @@ class ServeManager {
88
92
  * Throws if a mount with the same name already exists, preserving the
89
93
  * pre-existing semantics that callers may depend on.
90
94
  */
91
- async addMount(name, directory, sessionId, access = "owner", ownerEmail) {
95
+ async addMount(name, directory, sessionId, access = "link", ownerEmail) {
92
96
  if (this.mounts.has(name)) {
93
97
  throw new Error(`Mount '${name}' already exists. Remove it first or choose a different name.`);
94
98
  }
@@ -123,13 +127,18 @@ class ServeManager {
123
127
  if (this.mounts.has(spec.name)) {
124
128
  await this.removeMount(spec.name);
125
129
  }
130
+ const access = spec.access ?? "link";
126
131
  const mount = {
127
132
  name: spec.name,
128
133
  directory: resolvedDir,
129
134
  process: spec.process,
130
135
  sessionId: spec.sessionId,
131
136
  ownerEmail: spec.ownerEmail,
132
- access: spec.access ?? "owner",
137
+ access,
138
+ // Generate a capability token if access is 'link' and we don't
139
+ // already have one from persisted state (token is stable across
140
+ // restarts).
141
+ linkToken: access === "link" ? spec.linkToken || generateLinkToken() : void 0,
133
142
  addedAt: Date.now()
134
143
  };
135
144
  this.mounts.set(spec.name, mount);
@@ -432,11 +441,18 @@ class ServeManager {
432
441
  }, 3e4);
433
442
  }
434
443
  // ── Internal ─────────────────────────────────────────────────────────
435
- /** Get the public URL for a mount (mount-specific subdomain). */
444
+ /**
445
+ * Get the public URL for a mount (mount-specific subdomain).
446
+ * For `access: 'link'` mounts, the subdomain itself carries the capability —
447
+ * the tunnel registers a long random suffix (~128 bits of entropy), so the
448
+ * URL is identical in shape to other tiers, just longer. Content serves
449
+ * from the root, so HTML with absolute paths (`/style.css`, `/app.js`)
450
+ * works unchanged.
451
+ */
436
452
  getMountUrl(name) {
437
453
  const tunnel = this.mountTunnels.get(name);
438
- const url = tunnel?.getUrls().get(this.port);
439
- if (url) return `${url}/`;
454
+ const tunnelUrl = tunnel?.getUrls().get(this.port);
455
+ if (tunnelUrl) return `${tunnelUrl}/`;
440
456
  if (this.port) return `http://127.0.0.1:${this.port}/${name}/`;
441
457
  return null;
442
458
  }
@@ -523,7 +539,7 @@ class ServeManager {
523
539
  res.end(html);
524
540
  return;
525
541
  }
526
- if (mount && mount.access !== "public") {
542
+ if (mount && mount.access !== "public" && mount.access !== "link") {
527
543
  const userEmail = this.auth ? await this.auth.authenticate(req).catch(() => null) : null;
528
544
  const allowed = this.auth ? this.auth.isAuthorized(userEmail, mount.access, mount.ownerEmail) : false;
529
545
  if (!allowed) {
@@ -668,12 +684,15 @@ class ServeManager {
668
684
  if (!this.port) throw new Error("Auth proxy not running \u2014 call ensureRunning() first");
669
685
  const subdomainSafe = mountName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
670
686
  const tunnelName = `static-${subdomainSafe}`;
687
+ const mount = this.mounts.get(mountName);
688
+ const subdomainOverride = mount?.access === "link" && mount.linkToken ? /* @__PURE__ */ new Map([[this.port, `static-${subdomainSafe}-${mount.linkToken}`]]) : void 0;
671
689
  try {
672
690
  const { FrpcTunnel } = await import('./frpc-j60b46eU.mjs');
673
691
  let tunnel;
674
692
  tunnel = new FrpcTunnel({
675
693
  name: tunnelName,
676
694
  ports: [this.port],
695
+ subdomains: subdomainOverride,
677
696
  // End-to-end probe: the daemon's health loop watches probe.ok
678
697
  // to detect ghosted tunnel registrations (frpc says "connected"
679
698
  // but no traffic actually flows). The sentinel route is served
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svamp-cli",
3
- "version": "0.2.60",
3
+ "version": "0.2.65",
4
4
  "description": "Svamp CLI — AI workspace daemon on Hypha Cloud",
5
5
  "author": "Amun AI AB",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -20,7 +20,7 @@
20
20
  "scripts": {
21
21
  "build": "rm -rf dist bin/skills && mkdir -p bin/skills && cp -r ../../skills/artifact bin/skills/artifact && tsc --noEmit && pkgroll",
22
22
  "typecheck": "tsc --noEmit",
23
- "test": "npx tsx test/test-authorize.mjs && npx tsx test/test-normalize-allowed-user.mjs && npx tsx test/test-share-url.mjs && npx tsx test/test-update-sharing-normalization.mjs && npx tsx test/test-staged-homes-sweep.mjs && npx tsx test/test-session-helpers.mjs && npx tsx test/test-cli-routing.mjs && npx tsx test/test-security-context.mjs && npx tsx test/test-isolation-decision.mjs && npx tsx test/test-ralph-loop.mjs && npx tsx test/test-message-helpers.mjs && npx tsx test/test-agent-config.mjs && npx tsx test/test-wrap-command.mjs && npx tsx test/test-credential-staging.mjs && npx tsx test/test-claude-auth.mjs && npx tsx test/test-output-formatters.mjs && npx tsx test/test-agent-types.mjs && npx tsx test/test-transport.mjs && npx tsx test/test-session-update-handlers.mjs && npx tsx test/test-session-scanner.mjs && npx tsx test/test-hypha-client.mjs && npx tsx test/test-hook-settings.mjs && npx tsx test/test-session-service-logic.mjs && npx tsx test/test-daemon-persistence.mjs && npx tsx test/test-detect-isolation.mjs && npx tsx test/test-machine-service-logic.mjs && npx tsx test/test-interactive-helpers.mjs && npx tsx test/test-codex-backend.mjs && npx tsx test/test-acp-backend.mjs && npx tsx test/test-acp-bridge.mjs && npx tsx test/test-hook-server.mjs && npx tsx test/test-session-commands.mjs && npx tsx test/test-interactive-console.mjs && npx tsx test/test-session-messages.mjs && npx tsx test/test-skills.mjs && npx tsx test/test-agent-grouping.mjs && npx tsx test/test-ralph-loop-integration.mjs && npx tsx test/test-ralph-loop-modes.mjs && npx tsx test/test-machine-list-directory.mjs && npx tsx test/test-service-commands.mjs && npx tsx test/test-supervisor.mjs && npx tsx test/test-supervisor-lock.mjs && npx tsx test/test-clear-detection.mjs && npx tsx test/test-session-consolidation.mjs && npx tsx test/test-inbox.mjs && npx tsx test/test-session-rpc-dispatch.mjs && npx tsx test/test-sandbox-cli.mjs && npx tsx test/test-serve-manager.mjs && npx tsx test/test-serve-stability.mjs && npx tsx test/test-frpc-e2e.mjs --unit-only",
23
+ "test": "npx tsx test/test-context-window.mjs && npx tsx test/test-authorize.mjs && npx tsx test/test-normalize-allowed-user.mjs && npx tsx test/test-share-url.mjs && npx tsx test/test-update-sharing-normalization.mjs && npx tsx test/test-staged-homes-sweep.mjs && npx tsx test/test-session-helpers.mjs && npx tsx test/test-cli-routing.mjs && npx tsx test/test-security-context.mjs && npx tsx test/test-isolation-decision.mjs && npx tsx test/test-ralph-loop.mjs && npx tsx test/test-message-helpers.mjs && npx tsx test/test-agent-config.mjs && npx tsx test/test-wrap-command.mjs && npx tsx test/test-credential-staging.mjs && npx tsx test/test-claude-auth.mjs && npx tsx test/test-output-formatters.mjs && npx tsx test/test-agent-types.mjs && npx tsx test/test-transport.mjs && npx tsx test/test-session-update-handlers.mjs && npx tsx test/test-session-scanner.mjs && npx tsx test/test-hypha-client.mjs && npx tsx test/test-hook-settings.mjs && npx tsx test/test-session-service-logic.mjs && npx tsx test/test-daemon-persistence.mjs && npx tsx test/test-detect-isolation.mjs && npx tsx test/test-machine-service-logic.mjs && npx tsx test/test-interactive-helpers.mjs && npx tsx test/test-codex-backend.mjs && npx tsx test/test-acp-backend.mjs && npx tsx test/test-acp-bridge.mjs && npx tsx test/test-hook-server.mjs && npx tsx test/test-session-commands.mjs && npx tsx test/test-interactive-console.mjs && npx tsx test/test-session-messages.mjs && npx tsx test/test-skills.mjs && npx tsx test/test-agent-grouping.mjs && npx tsx test/test-ralph-loop-integration.mjs && npx tsx test/test-ralph-loop-modes.mjs && npx tsx test/test-machine-list-directory.mjs && npx tsx test/test-service-commands.mjs && npx tsx test/test-supervisor.mjs && npx tsx test/test-supervisor-lock.mjs && npx tsx test/test-clear-detection.mjs && npx tsx test/test-session-consolidation.mjs && npx tsx test/test-inbox.mjs && npx tsx test/test-session-rpc-dispatch.mjs && npx tsx test/test-sandbox-cli.mjs && npx tsx test/test-serve-manager.mjs && npx tsx test/test-serve-stability.mjs && npx tsx test/test-frpc-e2e.mjs --unit-only",
24
24
  "test:hypha": "node --no-warnings test/test-hypha-service.mjs",
25
25
  "dev": "tsx src/cli.ts",
26
26
  "dev:daemon": "tsx src/cli.ts daemon start-sync",
@@ -1,63 +0,0 @@
1
- var name = "svamp-cli";
2
- var version = "0.2.60";
3
- var description = "Svamp CLI — AI workspace daemon on Hypha Cloud";
4
- var author = "Amun AI AB";
5
- var license = "SEE LICENSE IN LICENSE";
6
- var type = "module";
7
- var bin = {
8
- svamp: "./bin/svamp.mjs"
9
- };
10
- var files = [
11
- "dist",
12
- "bin"
13
- ];
14
- var main = "./dist/index.mjs";
15
- var exports$1 = {
16
- ".": "./dist/index.mjs",
17
- "./cli": "./dist/cli.mjs"
18
- };
19
- var scripts = {
20
- build: "rm -rf dist bin/skills && mkdir -p bin/skills && cp -r ../../skills/artifact bin/skills/artifact && tsc --noEmit && pkgroll",
21
- typecheck: "tsc --noEmit",
22
- test: "npx tsx test/test-authorize.mjs && npx tsx test/test-normalize-allowed-user.mjs && npx tsx test/test-share-url.mjs && npx tsx test/test-update-sharing-normalization.mjs && npx tsx test/test-staged-homes-sweep.mjs && npx tsx test/test-session-helpers.mjs && npx tsx test/test-cli-routing.mjs && npx tsx test/test-security-context.mjs && npx tsx test/test-isolation-decision.mjs && npx tsx test/test-ralph-loop.mjs && npx tsx test/test-message-helpers.mjs && npx tsx test/test-agent-config.mjs && npx tsx test/test-wrap-command.mjs && npx tsx test/test-credential-staging.mjs && npx tsx test/test-claude-auth.mjs && npx tsx test/test-output-formatters.mjs && npx tsx test/test-agent-types.mjs && npx tsx test/test-transport.mjs && npx tsx test/test-session-update-handlers.mjs && npx tsx test/test-session-scanner.mjs && npx tsx test/test-hypha-client.mjs && npx tsx test/test-hook-settings.mjs && npx tsx test/test-session-service-logic.mjs && npx tsx test/test-daemon-persistence.mjs && npx tsx test/test-detect-isolation.mjs && npx tsx test/test-machine-service-logic.mjs && npx tsx test/test-interactive-helpers.mjs && npx tsx test/test-codex-backend.mjs && npx tsx test/test-acp-backend.mjs && npx tsx test/test-acp-bridge.mjs && npx tsx test/test-hook-server.mjs && npx tsx test/test-session-commands.mjs && npx tsx test/test-interactive-console.mjs && npx tsx test/test-session-messages.mjs && npx tsx test/test-skills.mjs && npx tsx test/test-agent-grouping.mjs && npx tsx test/test-ralph-loop-integration.mjs && npx tsx test/test-ralph-loop-modes.mjs && npx tsx test/test-machine-list-directory.mjs && npx tsx test/test-service-commands.mjs && npx tsx test/test-supervisor.mjs && npx tsx test/test-supervisor-lock.mjs && npx tsx test/test-clear-detection.mjs && npx tsx test/test-session-consolidation.mjs && npx tsx test/test-inbox.mjs && npx tsx test/test-session-rpc-dispatch.mjs && npx tsx test/test-sandbox-cli.mjs && npx tsx test/test-serve-manager.mjs && npx tsx test/test-serve-stability.mjs && npx tsx test/test-frpc-e2e.mjs --unit-only",
23
- "test:hypha": "node --no-warnings test/test-hypha-service.mjs",
24
- dev: "tsx src/cli.ts",
25
- "dev:daemon": "tsx src/cli.ts daemon start-sync",
26
- "test:e2e": "node --no-warnings test/e2e-session-tests.mjs",
27
- "test:frpc": "npx tsx test/test-frpc-e2e.mjs"
28
- };
29
- var dependencies = {
30
- "@agentclientprotocol/sdk": "^0.14.1",
31
- "@modelcontextprotocol/sdk": "^1.25.3",
32
- "hypha-rpc": "0.21.40",
33
- "node-pty": "1.2.0-beta.11",
34
- ws: "^8.18.0",
35
- yaml: "^2.8.2",
36
- zod: "^3.24.4"
37
- };
38
- var devDependencies = {
39
- "@types/node": ">=20",
40
- "@types/ws": "^8.5.14",
41
- pkgroll: "^2.14.2",
42
- tsx: "^4.20.6",
43
- typescript: "5.9.3"
44
- };
45
- var packageManager = "yarn@1.22.22";
46
- var _package = {
47
- name: name,
48
- version: version,
49
- description: description,
50
- author: author,
51
- license: license,
52
- type: type,
53
- bin: bin,
54
- files: files,
55
- main: main,
56
- exports: exports$1,
57
- scripts: scripts,
58
- dependencies: dependencies,
59
- devDependencies: devDependencies,
60
- packageManager: packageManager
61
- };
62
-
63
- export { author, bin, _package as default, dependencies, description, devDependencies, exports$1 as exports, files, license, main, name, packageManager, scripts, type, version };