harnery 0.41.0 → 0.42.0
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/dist/commander.js +3 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +9 -1
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +113 -12
- package/dist/commands/browse.d.ts +164 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +263 -51
- package/dist/commands/claude-desktop.d.ts.map +1 -1
- package/dist/commands/claude-desktop.js +52 -0
- package/dist/commands/disk.d.ts +6 -0
- package/dist/commands/disk.d.ts.map +1 -0
- package/dist/commands/disk.js +80 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +48 -1
- package/dist/commands/files.d.ts +16 -0
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +54 -2
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +43 -9
- package/dist/commands/rm.d.ts +4 -0
- package/dist/commands/rm.d.ts.map +1 -0
- package/dist/commands/rm.js +87 -0
- package/dist/commands/servers.d.ts +7 -0
- package/dist/commands/servers.d.ts.map +1 -0
- package/dist/commands/servers.js +394 -0
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +82 -2
- package/dist/core/adapters/profiles.d.ts +9 -0
- package/dist/core/adapters/profiles.d.ts.map +1 -1
- package/dist/core/adapters/profiles.js +22 -1
- package/dist/core/adapters/types.d.ts +9 -0
- package/dist/core/adapters/types.d.ts.map +1 -1
- package/dist/core/agents/cli.js +13 -6
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +2 -0
- package/dist/core/agents/state/live-coordination-view.d.ts +20 -0
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +60 -0
- package/dist/core/artifacts/delivery-card.js +9 -2
- package/dist/core/artifacts/index.d.ts +140 -4
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +522 -49
- package/dist/core/config.d.ts +68 -11
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +128 -3
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +2 -2
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +16 -0
- package/dist/core/events/v3/projection.js +9 -1
- package/dist/core/hooks/adapter/events.d.ts +17 -0
- package/dist/core/hooks/adapter/events.d.ts.map +1 -1
- package/dist/core/hooks/adapter/events.js +5 -0
- package/dist/core/hooks/adapter/runtime-telemetry.js +3 -1
- package/dist/core/hooks/adapter/wiring.d.ts +37 -7
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +62 -4
- package/dist/core/hooks/cli.js +45 -6
- package/dist/core/hooks/run-markers.d.ts +37 -0
- package/dist/core/hooks/run-markers.d.ts.map +1 -0
- package/dist/core/hooks/run-markers.js +109 -0
- package/dist/core/servers/index.d.ts +211 -0
- package/dist/core/servers/index.d.ts.map +1 -0
- package/dist/core/servers/index.js +560 -0
- package/dist/core/servers/net.d.ts +58 -0
- package/dist/core/servers/net.d.ts.map +1 -0
- package/dist/core/servers/net.js +270 -0
- package/dist/core/servers/tunnels.d.ts +14 -0
- package/dist/core/servers/tunnels.d.ts.map +1 -0
- package/dist/core/servers/tunnels.js +33 -0
- package/dist/core/storage/builtins.js +1 -0
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +30 -1
- package/dist/core/workflow/sandbox-projection.d.ts +13 -2
- package/dist/core/workflow/sandbox-projection.d.ts.map +1 -1
- package/dist/core/workflow/sandbox-projection.js +23 -0
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +6 -1
- package/dist/core/workflow/spawn-codex.d.ts +2 -0
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +20 -1
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +6 -1
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
- package/dist/core/workflow/spawn-opencode.js +4 -1
- package/dist/core/workflow/types.d.ts +47 -2
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/worker-access.d.ts +92 -0
- package/dist/core/workflow/worker-access.d.ts.map +1 -0
- package/dist/core/workflow/worker-access.js +450 -0
- package/dist/lib/browser/batch-steps.d.ts +18 -0
- package/dist/lib/browser/batch-steps.d.ts.map +1 -0
- package/dist/lib/browser/batch-steps.js +82 -0
- package/dist/lib/browser/collect/collector.d.ts +99 -0
- package/dist/lib/browser/collect/collector.d.ts.map +1 -0
- package/dist/lib/browser/collect/collector.js +202 -0
- package/dist/lib/browser/collect/fields.d.ts +31 -0
- package/dist/lib/browser/collect/fields.d.ts.map +1 -0
- package/dist/lib/browser/collect/fields.js +75 -0
- package/dist/lib/browser/collect/index.d.ts +6 -0
- package/dist/lib/browser/collect/index.d.ts.map +1 -0
- package/dist/lib/browser/collect/index.js +5 -0
- package/dist/lib/browser/collect/page-driver.d.ts +14 -0
- package/dist/lib/browser/collect/page-driver.d.ts.map +1 -0
- package/dist/lib/browser/collect/page-driver.js +306 -0
- package/dist/lib/browser/collect/presets.d.ts +52 -0
- package/dist/lib/browser/collect/presets.d.ts.map +1 -0
- package/dist/lib/browser/collect/presets.js +176 -0
- package/dist/lib/browser/collect/scroll-plan.d.ts +85 -0
- package/dist/lib/browser/collect/scroll-plan.d.ts.map +1 -0
- package/dist/lib/browser/collect/scroll-plan.js +118 -0
- package/dist/lib/browser/index.d.ts +1 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.d.ts +2 -0
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.js +13 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +10 -0
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +1 -0
- package/dist/lib/browser/windows-chrome.d.ts +59 -0
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -0
- package/dist/lib/browser/windows-chrome.js +179 -0
- package/dist/lib/claude-desktop-share.d.ts +136 -0
- package/dist/lib/claude-desktop-share.d.ts.map +1 -0
- package/dist/lib/claude-desktop-share.js +394 -0
- package/dist/lib/disk-usage.d.ts +50 -0
- package/dist/lib/disk-usage.d.ts.map +1 -0
- package/dist/lib/disk-usage.js +303 -0
- package/dist/lib/docs-lint.d.ts +6 -0
- package/dist/lib/docs-lint.d.ts.map +1 -1
- package/dist/lib/docs-lint.js +8 -3
- package/dist/lib/guarded-remove.d.ts +34 -0
- package/dist/lib/guarded-remove.d.ts.map +1 -0
- package/dist/lib/guarded-remove.js +310 -0
- package/dist/lib/instructions/templates.js +8 -8
- package/dist/lib/tunnel/error-page.d.ts +1 -1
- package/dist/lib/tunnel/error-page.d.ts.map +1 -1
- package/dist/lib/tunnel/error-page.js +19 -8
- package/dist/lib/tunnel/gate.js +20 -1
- package/dist/lib/tunnel/path-scope.d.ts +22 -0
- package/dist/lib/tunnel/path-scope.d.ts.map +1 -0
- package/dist/lib/tunnel/path-scope.js +62 -0
- package/dist/lib/tunnel/state.d.ts +5 -0
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +17 -3
- package/package.json +6 -1
- package/schemas/config.schema.json +91 -0
- package/src/commander.ts +19 -0
- package/src/commands/agents.ts +14 -0
- package/src/commands/artifacts.ts +171 -19
- package/src/commands/browse.ts +400 -57
- package/src/commands/claude-desktop.ts +61 -0
- package/src/commands/disk.ts +114 -0
- package/src/commands/doctor.ts +52 -1
- package/src/commands/files.ts +93 -2
- package/src/commands/init.ts +55 -5
- package/src/commands/rm.ts +115 -0
- package/src/commands/servers.ts +506 -0
- package/src/commands/tunnel.ts +102 -2
- package/src/core/adapters/profiles.ts +22 -1
- package/src/core/adapters/types.ts +10 -0
- package/src/core/agents/cli.ts +14 -6
- package/src/core/agents/render/session-context.ts +1 -0
- package/src/core/agents/state/live-coordination-view.ts +73 -0
- package/src/core/artifacts/delivery-card.ts +10 -2
- package/src/core/artifacts/index.ts +697 -44
- package/src/core/config.ts +183 -4
- package/src/core/diagnostics/bundle.ts +2 -1
- package/src/core/events/v3/producers/recorder.ts +16 -0
- package/src/core/events/v3/projection.ts +11 -1
- package/src/core/hooks/adapter/events.ts +19 -0
- package/src/core/hooks/adapter/runtime-telemetry.ts +3 -1
- package/src/core/hooks/adapter/wiring.ts +116 -8
- package/src/core/hooks/cli.ts +47 -6
- package/src/core/hooks/run-markers.ts +132 -0
- package/src/core/servers/index.ts +712 -0
- package/src/core/servers/net.ts +283 -0
- package/src/core/servers/tunnels.ts +39 -0
- package/src/core/storage/builtins.ts +1 -0
- package/src/core/workflow/engine.ts +38 -1
- package/src/core/workflow/sandbox-projection.ts +38 -2
- package/src/core/workflow/spawn-claude.ts +5 -1
- package/src/core/workflow/spawn-codex.ts +18 -1
- package/src/core/workflow/spawn-cursor.ts +5 -1
- package/src/core/workflow/spawn-opencode.ts +4 -1
- package/src/core/workflow/types.ts +62 -2
- package/src/core/workflow/worker-access.ts +525 -0
- package/src/lib/browser/batch-steps.ts +79 -0
- package/src/lib/browser/collect/collector.ts +306 -0
- package/src/lib/browser/collect/fields.ts +87 -0
- package/src/lib/browser/collect/index.ts +47 -0
- package/src/lib/browser/collect/page-driver.ts +327 -0
- package/src/lib/browser/collect/presets.ts +237 -0
- package/src/lib/browser/collect/scroll-plan.ts +179 -0
- package/src/lib/browser/index.ts +1 -0
- package/src/lib/browser/page-review-judge.ts +16 -0
- package/src/lib/browser/qa-run-contracts.ts +11 -0
- package/src/lib/browser/qa-run.ts +1 -0
- package/src/lib/browser/windows-chrome.ts +227 -0
- package/src/lib/claude-desktop-share.ts +511 -0
- package/src/lib/disk-usage.ts +347 -0
- package/src/lib/docs-lint.ts +8 -3
- package/src/lib/guarded-remove.ts +376 -0
- package/src/lib/instructions/templates.ts +8 -8
- package/src/lib/tunnel/error-page.ts +25 -9
- package/src/lib/tunnel/gate.ts +22 -1
- package/src/lib/tunnel/path-scope.ts +73 -0
- package/src/lib/tunnel/state.ts +20 -3
package/src/commands/tunnel.ts
CHANGED
|
@@ -4,12 +4,20 @@ import { resolve } from "node:path";
|
|
|
4
4
|
import type { Command } from "commander";
|
|
5
5
|
import type { EmitContext, HarneryProgramContext } from "../commander.ts";
|
|
6
6
|
|
|
7
|
+
import { resolveCoordRoot } from "../core/agents/coord-client.ts";
|
|
7
8
|
import { resolveBinName } from "../core/config.ts";
|
|
9
|
+
import {
|
|
10
|
+
tryRegisterServer,
|
|
11
|
+
tunnelServerId,
|
|
12
|
+
tunnelServerInput,
|
|
13
|
+
unregisterServer,
|
|
14
|
+
} from "../core/servers/index.ts";
|
|
8
15
|
import {
|
|
9
16
|
processLogDestination,
|
|
10
17
|
runRotatingProcessSync,
|
|
11
18
|
spawnRotatingProcess,
|
|
12
19
|
} from "../core/storage/process-log.ts";
|
|
20
|
+
import { ALLOW_PATHS_ENV, normalizeAllowPaths } from "../lib/tunnel/path-scope.ts";
|
|
13
21
|
import {
|
|
14
22
|
clearState,
|
|
15
23
|
DEFAULT_INSTANCE,
|
|
@@ -35,6 +43,8 @@ import {
|
|
|
35
43
|
* upstream. Cloudflare quick tunnels add an IP allowlist at the gate via the
|
|
36
44
|
* Cloudflare-set `CF-Connecting-IP` header; Tailscale Serve/Funnel exposes the
|
|
37
45
|
* same gate through tailscaled, with Tailscale owning the access boundary.
|
|
46
|
+
* Every provider also passes through a path scope: `up` requires at least one
|
|
47
|
+
* `--allow-path <prefix>`, and the gate refuses any request outside it.
|
|
38
48
|
*
|
|
39
49
|
* State + config persisted under `.cache/tunnel/`. cloudflared auto-installs
|
|
40
50
|
* to ~/.local/bin/ on first run (Linux only; macOS users `brew install`);
|
|
@@ -55,6 +65,7 @@ interface UpOpts {
|
|
|
55
65
|
visibility?: string;
|
|
56
66
|
path?: string;
|
|
57
67
|
httpsPort?: string;
|
|
68
|
+
allowPath?: string[];
|
|
58
69
|
}
|
|
59
70
|
|
|
60
71
|
interface DownOpts {
|
|
@@ -165,6 +176,46 @@ function resolveHttpsPort(raw: string | undefined): number {
|
|
|
165
176
|
return port;
|
|
166
177
|
}
|
|
167
178
|
|
|
179
|
+
/**
|
|
180
|
+
* The path scope for `up`. At least one prefix is required: a tunnel forwards
|
|
181
|
+
* to a whole local app, and publishing all of it has to be a stated choice
|
|
182
|
+
* (`--allow-path /`), never the result of leaving a flag off.
|
|
183
|
+
*/
|
|
184
|
+
function resolveAllowPaths(raw: string[] | undefined): string[] {
|
|
185
|
+
let paths: string[];
|
|
186
|
+
try {
|
|
187
|
+
paths = normalizeAllowPaths(raw ?? []);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
emit.error({
|
|
190
|
+
code: "tunnel_path_scope_invalid",
|
|
191
|
+
message: error instanceof Error ? error.message : String(error),
|
|
192
|
+
});
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
if (paths.length === 0) {
|
|
196
|
+
emit.error({
|
|
197
|
+
code: "tunnel_path_scope_empty",
|
|
198
|
+
message:
|
|
199
|
+
"No path scope; refusing to start. Name what the tunnel shares with --allow-path <prefix> " +
|
|
200
|
+
"(repeatable), or pass --allow-path / to publish the whole upstream.",
|
|
201
|
+
});
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
return paths;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function samePaths(a: readonly string[], b: readonly string[]): boolean {
|
|
208
|
+
return a.length === b.length && [...a].sort().join("\n") === [...b].sort().join("\n");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function formatPaths(paths: readonly string[]): string {
|
|
212
|
+
return paths.length === 0 ? "(none: every path refused)" : paths.join(", ");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function collectPath(value: string, previous: string[]): string[] {
|
|
216
|
+
return [...previous, value];
|
|
217
|
+
}
|
|
218
|
+
|
|
168
219
|
function gateScriptPath(): string {
|
|
169
220
|
return resolve(import.meta.dirname, "..", "lib", "tunnel", "gate.ts");
|
|
170
221
|
}
|
|
@@ -444,6 +495,7 @@ interface GateSpawnOpts {
|
|
|
444
495
|
vhost: string;
|
|
445
496
|
provider: TunnelProvider;
|
|
446
497
|
allowedIps: string[];
|
|
498
|
+
allowPaths: string[];
|
|
447
499
|
gateLogPath: string;
|
|
448
500
|
}
|
|
449
501
|
|
|
@@ -470,6 +522,7 @@ function spawnGate(o: GateSpawnOpts): ChildProcess {
|
|
|
470
522
|
HARNERY_TUNNEL_TARGET: o.target,
|
|
471
523
|
HARNERY_TUNNEL_VHOST: o.vhost,
|
|
472
524
|
HARNERY_TUNNEL_PORT: String(o.gatePort),
|
|
525
|
+
[ALLOW_PATHS_ENV]: o.allowPaths.join(","),
|
|
473
526
|
},
|
|
474
527
|
});
|
|
475
528
|
gateProc.unref();
|
|
@@ -498,6 +551,7 @@ async function up(opts: UpOpts): Promise<void> {
|
|
|
498
551
|
provider === "tailscale" ? resolveTailscalePath(opts.path, name) : undefined;
|
|
499
552
|
const tailscaleHttpsPort =
|
|
500
553
|
provider === "tailscale" ? resolveHttpsPort(opts.httpsPort) : undefined;
|
|
554
|
+
const allowPaths = resolveAllowPaths(opts.allowPath);
|
|
501
555
|
|
|
502
556
|
const existing = readState(name);
|
|
503
557
|
if (existing && tunnelIsAlive(existing)) {
|
|
@@ -508,9 +562,19 @@ async function up(opts: UpOpts): Promise<void> {
|
|
|
508
562
|
});
|
|
509
563
|
process.exit(1);
|
|
510
564
|
}
|
|
565
|
+
if (!samePaths(existing.allow_paths, allowPaths)) {
|
|
566
|
+
emit.error({
|
|
567
|
+
code: "tunnel_instance_in_use",
|
|
568
|
+
message:
|
|
569
|
+
`Tunnel [${name}] is already up sharing ${formatPaths(existing.allow_paths)}. ` +
|
|
570
|
+
`Stop it before starting it with ${formatPaths(allowPaths)}.`,
|
|
571
|
+
});
|
|
572
|
+
process.exit(1);
|
|
573
|
+
}
|
|
511
574
|
emit.text(`Already up [${name}]: ${existing.url}\n`);
|
|
512
575
|
emit.text(` Provider: ${existing.provider}\n`);
|
|
513
576
|
emit.text(` Forwarding: ${existing.target} (Host: ${existing.vhost})\n`);
|
|
577
|
+
emit.text(` Paths: ${formatPaths(existing.allow_paths)}\n`);
|
|
514
578
|
return;
|
|
515
579
|
}
|
|
516
580
|
if (existing) downOne(name, new Set());
|
|
@@ -552,6 +616,7 @@ async function up(opts: UpOpts): Promise<void> {
|
|
|
552
616
|
vhost,
|
|
553
617
|
provider,
|
|
554
618
|
allowedIps: cfg.allowed_ips,
|
|
619
|
+
allowPaths,
|
|
555
620
|
gateLogPath,
|
|
556
621
|
});
|
|
557
622
|
|
|
@@ -630,11 +695,13 @@ async function up(opts: UpOpts): Promise<void> {
|
|
|
630
695
|
target,
|
|
631
696
|
vhost,
|
|
632
697
|
gate_port: gatePort,
|
|
698
|
+
allow_paths: allowPaths,
|
|
633
699
|
tailscale_mode: tailscaleMode,
|
|
634
700
|
tailscale_path: tailscalePath,
|
|
635
701
|
tailscale_https_port: tailscaleHttpsPort,
|
|
636
702
|
};
|
|
637
703
|
writeState(state);
|
|
704
|
+
recordTunnelServer(state);
|
|
638
705
|
|
|
639
706
|
const stopHint =
|
|
640
707
|
name === DEFAULT_INSTANCE ? "harn tunnel down" : `harn tunnel down --name ${name}`;
|
|
@@ -642,6 +709,7 @@ async function up(opts: UpOpts): Promise<void> {
|
|
|
642
709
|
emit.text(` Provider: ${provider}${tailscaleMode ? ` (${tailscaleMode})` : ""}\n`);
|
|
643
710
|
emit.text(` URL: ${url}\n\n`);
|
|
644
711
|
emit.text(` Forwarding: ${target} (Host: ${vhost})\n`);
|
|
712
|
+
emit.text(` Paths: ${formatPaths(allowPaths)}\n`);
|
|
645
713
|
emit.text(` Gate port: ${gatePort}\n`);
|
|
646
714
|
if (provider === "cloudflare") {
|
|
647
715
|
emit.text(` Allowed IPs: ${cfg.allowed_ips.join(", ")}\n\n`);
|
|
@@ -662,6 +730,16 @@ async function up(opts: UpOpts): Promise<void> {
|
|
|
662
730
|
emit.text(" Status: harn tunnel status\n");
|
|
663
731
|
}
|
|
664
732
|
|
|
733
|
+
/** The server registry lists tunnels beside every other local server. */
|
|
734
|
+
function registryRoot(): string {
|
|
735
|
+
return resolveCoordRoot() ?? process.cwd();
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function recordTunnelServer(state: TunnelState): void {
|
|
739
|
+
const coordRoot = registryRoot();
|
|
740
|
+
tryRegisterServer(tunnelServerInput(state, coordRoot), { coordRoot });
|
|
741
|
+
}
|
|
742
|
+
|
|
665
743
|
/** Tear down a single instance by name. Returns the number of processes killed. */
|
|
666
744
|
function downOne(name: string, killed: Set<number>): number {
|
|
667
745
|
const before = killed.size;
|
|
@@ -684,6 +762,7 @@ function downOne(name: string, killed: Set<number>): number {
|
|
|
684
762
|
// lost; they'd otherwise squat on the port and break the next `up`.
|
|
685
763
|
sweepStrays(state?.gate_port ?? DEFAULT_GATE_PORT, killed);
|
|
686
764
|
clearState(name);
|
|
765
|
+
unregisterServer(tunnelServerId(name), { coordRoot: registryRoot() });
|
|
687
766
|
return killed.size - before;
|
|
688
767
|
}
|
|
689
768
|
|
|
@@ -771,6 +850,15 @@ export async function reloadOne(state: TunnelState): Promise<{ ok: boolean; mess
|
|
|
771
850
|
if (!bunAvailable()) {
|
|
772
851
|
return { ok: false, message: `[${name}] bun is not on PATH, so the gate can't be respawned.` };
|
|
773
852
|
}
|
|
853
|
+
if (state.allow_paths.length === 0) {
|
|
854
|
+
return {
|
|
855
|
+
ok: false,
|
|
856
|
+
message:
|
|
857
|
+
`[${name}] has no recorded path scope, so a reloaded gate would refuse every request. ` +
|
|
858
|
+
`Restart it with \`${bin} tunnel down --name ${name}\` and ` +
|
|
859
|
+
`\`${bin} tunnel up --name ${name} --allow-path <prefix>\`.`,
|
|
860
|
+
};
|
|
861
|
+
}
|
|
774
862
|
|
|
775
863
|
// Kill ONLY the gate. sweepStrays() is deliberately avoided here: it also
|
|
776
864
|
// matches `--url http://localhost:<port>`, which is the provider process we
|
|
@@ -803,6 +891,7 @@ export async function reloadOne(state: TunnelState): Promise<{ ok: boolean; mess
|
|
|
803
891
|
vhost: state.vhost,
|
|
804
892
|
provider: state.provider,
|
|
805
893
|
allowedIps: cfg.allowed_ips,
|
|
894
|
+
allowPaths: state.allow_paths,
|
|
806
895
|
gateLogPath: tunnelLogDestinations(name, state.provider).gate,
|
|
807
896
|
});
|
|
808
897
|
|
|
@@ -822,6 +911,7 @@ export async function reloadOne(state: TunnelState): Promise<{ ok: boolean; mess
|
|
|
822
911
|
}
|
|
823
912
|
|
|
824
913
|
writeState({ ...state, gate_pid: pid });
|
|
914
|
+
recordTunnelServer({ ...state, gate_pid: pid });
|
|
825
915
|
const allowNote =
|
|
826
916
|
state.provider === "cloudflare" ? ` Allowlist: ${cfg.allowed_ips.length} IP(s).` : "";
|
|
827
917
|
return { ok: true, message: `[${name}] gate reloaded, URL unchanged: ${state.url}.${allowNote}` };
|
|
@@ -898,6 +988,7 @@ function statusDetail(state: TunnelState): void {
|
|
|
898
988
|
);
|
|
899
989
|
emit.text(` URL: ${state.url}\n`);
|
|
900
990
|
emit.text(` Forwarding: ${state.target} (Host: ${state.vhost})\n`);
|
|
991
|
+
emit.text(` Paths: ${formatPaths(state.allow_paths)}\n`);
|
|
901
992
|
emit.text(` Gate port: ${state.gate_port}\n`);
|
|
902
993
|
if (state.provider === "cloudflare") {
|
|
903
994
|
emit.text(` Allowed IPs: ${cfg.allowed_ips.join(", ")}\n`);
|
|
@@ -941,6 +1032,7 @@ function status(opts: StatusOpts): void {
|
|
|
941
1032
|
state: instanceState(s),
|
|
942
1033
|
url: s.url,
|
|
943
1034
|
fwd: `${s.target} (${s.vhost})`,
|
|
1035
|
+
paths: formatPaths(s.allow_paths),
|
|
944
1036
|
port: String(s.gate_port),
|
|
945
1037
|
up: fmtUptime(s.started_at),
|
|
946
1038
|
}));
|
|
@@ -950,15 +1042,16 @@ function status(opts: StatusOpts): void {
|
|
|
950
1042
|
state: 5,
|
|
951
1043
|
url: Math.max(3, ...rows.map((r) => r.url.length)),
|
|
952
1044
|
fwd: Math.max(10, ...rows.map((r) => r.fwd.length)),
|
|
1045
|
+
paths: Math.max(5, ...rows.map((r) => r.paths.length)),
|
|
953
1046
|
port: 4,
|
|
954
1047
|
};
|
|
955
1048
|
const pad = (s: string, n: number) => s.padEnd(n);
|
|
956
1049
|
emit.text(
|
|
957
|
-
`${pad("NAME", w.name)} ${pad("PROVIDER", w.provider)} ${pad("STATE", w.state)} ${pad("URL", w.url)} ${pad("FORWARDING", w.fwd)} ${pad("PORT", w.port)} UPTIME\n`,
|
|
1050
|
+
`${pad("NAME", w.name)} ${pad("PROVIDER", w.provider)} ${pad("STATE", w.state)} ${pad("URL", w.url)} ${pad("FORWARDING", w.fwd)} ${pad("PATHS", w.paths)} ${pad("PORT", w.port)} UPTIME\n`,
|
|
958
1051
|
);
|
|
959
1052
|
for (const r of rows) {
|
|
960
1053
|
emit.text(
|
|
961
|
-
`${pad(r.name, w.name)} ${pad(r.provider, w.provider)} ${pad(r.state, w.state)} ${pad(r.url, w.url)} ${pad(r.fwd, w.fwd)} ${pad(r.port, w.port)} ${r.up}\n`,
|
|
1054
|
+
`${pad(r.name, w.name)} ${pad(r.provider, w.provider)} ${pad(r.state, w.state)} ${pad(r.url, w.url)} ${pad(r.fwd, w.fwd)} ${pad(r.paths, w.paths)} ${pad(r.port, w.port)} ${r.up}\n`,
|
|
962
1055
|
);
|
|
963
1056
|
}
|
|
964
1057
|
}
|
|
@@ -1071,6 +1164,13 @@ export function registerTunnelCommand(
|
|
|
1071
1164
|
"Tailscale only: URL path mount (default: / for default, /<name> for named instances)",
|
|
1072
1165
|
)
|
|
1073
1166
|
.option("--https-port <port>", "Tailscale only: HTTPS listen port (default: 443)", "443")
|
|
1167
|
+
.option(
|
|
1168
|
+
"--allow-path <prefix>",
|
|
1169
|
+
"URL path prefix the tunnel shares (repeatable, required). Every other path is refused; " +
|
|
1170
|
+
"pass / to publish the whole upstream.",
|
|
1171
|
+
collectPath,
|
|
1172
|
+
[],
|
|
1173
|
+
)
|
|
1074
1174
|
.action(up);
|
|
1075
1175
|
|
|
1076
1176
|
cmd
|
|
@@ -45,6 +45,18 @@ export const BUILTIN_ADAPTER_PROFILES = {
|
|
|
45
45
|
modelFamily: "claude",
|
|
46
46
|
effortValues: ["low", "medium", "high", "xhigh", "max"],
|
|
47
47
|
verified: { date: "2026-08-18", version: "2.1.233 (Claude Code)" },
|
|
48
|
+
// Checked against 2.1.283 (ADR 0192). Claude Code has no sandbox mode flag;
|
|
49
|
+
// its permission mode and its optional Bash sandbox are separate, so full
|
|
50
|
+
// access needs both: bypass permissions, and a settings layer that turns
|
|
51
|
+
// the Bash sandbox off in case a user or project settings file enabled it.
|
|
52
|
+
fullAccess: {
|
|
53
|
+
argv: [
|
|
54
|
+
"--permission-mode",
|
|
55
|
+
"bypassPermissions",
|
|
56
|
+
"--settings",
|
|
57
|
+
'{"sandbox":{"enabled":false}}',
|
|
58
|
+
],
|
|
59
|
+
},
|
|
48
60
|
capabilities: capabilities({
|
|
49
61
|
effortSelection: supported("Mapped to `--effort <level>`."),
|
|
50
62
|
maxTurns: supported("Mapped to `--max-turns <n>`."),
|
|
@@ -78,6 +90,9 @@ export const BUILTIN_ADAPTER_PROFILES = {
|
|
|
78
90
|
modes: { "read-only": "read-only", "workspace-write": "workspace-write" },
|
|
79
91
|
writableRoots: true,
|
|
80
92
|
},
|
|
93
|
+
// Checked against codex-cli 0.155.1 (ADR 0192): `--sandbox` accepts
|
|
94
|
+
// danger-full-access, and `codex exec` never prompts for approval.
|
|
95
|
+
fullAccess: { argv: ["--sandbox", "danger-full-access"] },
|
|
81
96
|
capabilities: capabilities({
|
|
82
97
|
effortSelection: supported('Mapped to `-c model_reasoning_effort="<level>"`.'),
|
|
83
98
|
filesystemPolicyProjection: supported(
|
|
@@ -107,6 +122,10 @@ export const BUILTIN_ADAPTER_PROFILES = {
|
|
|
107
122
|
modelFamily: "multi",
|
|
108
123
|
effortValues: [],
|
|
109
124
|
verified: { date: "2026-09-20", version: "2026.09.18-9a7762b" },
|
|
125
|
+
// Checked against 2026.09.26-dd393fe (ADR 0192): `--sandbox disabled`
|
|
126
|
+
// overrides the configured sandbox. Cursor carries effort inside the model
|
|
127
|
+
// id, so a full-access floor for it names exact model ids.
|
|
128
|
+
fullAccess: { argv: ["--sandbox", "disabled"] },
|
|
110
129
|
capabilities: capabilities({
|
|
111
130
|
effortSelection: unsupported(
|
|
112
131
|
"Cursor embeds effort in some parameterized model ids; Harnery does not rewrite model ids.",
|
|
@@ -136,7 +155,9 @@ export const BUILTIN_ADAPTER_PROFILES = {
|
|
|
136
155
|
authModel: "own-auth",
|
|
137
156
|
modelFamily: "multi",
|
|
138
157
|
// Model reasoning is selected through the `provider/model#variant` id;
|
|
139
|
-
// OpenCode exposes no separate effort dial for Harnery to map.
|
|
158
|
+
// OpenCode exposes no separate effort dial for Harnery to map. It also has
|
|
159
|
+
// no vendor sandbox to remove, so it declares no full-access rendering and
|
|
160
|
+
// a worker on it is never granted one (ADR 0192).
|
|
140
161
|
effortValues: [],
|
|
141
162
|
verified: { date: "2026-09-20", version: "opencode v2.0.11" },
|
|
142
163
|
capabilities: capabilities({
|
|
@@ -67,6 +67,16 @@ export interface AdapterProfile {
|
|
|
67
67
|
/** How this adapter projects host filesystem policy into the vendor's own
|
|
68
68
|
* sandbox (ADR 0039). Absent means it cannot project any of it. */
|
|
69
69
|
sandboxProjection?: AdapterSandboxProjection;
|
|
70
|
+
/** Vendor-native arguments that launch a worker with no vendor sandbox
|
|
71
|
+
* (ADR 0192). Absent means the adapter has no distinguishable full-access
|
|
72
|
+
* mode, so a worker on it never qualifies for one. */
|
|
73
|
+
fullAccess?: AdapterFullAccessRendering;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** How an adapter renders an unsandboxed launch (ADR 0192). The spawner emits
|
|
77
|
+
* exactly these arguments, so the declaration and the argv cannot drift. */
|
|
78
|
+
export interface AdapterFullAccessRendering {
|
|
79
|
+
argv: readonly string[];
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
/** Fully planned child invocation. `resultFile` is used by adapters such as
|
package/src/core/agents/cli.ts
CHANGED
|
@@ -309,12 +309,20 @@ async function handleStateAction(root: string, action: string, rest: string[]):
|
|
|
309
309
|
}
|
|
310
310
|
} catch (error) {
|
|
311
311
|
const detail = error instanceof Error ? error.message : String(error);
|
|
312
|
-
// A missing heartbeat
|
|
313
|
-
// while its process lived on
|
|
314
|
-
//
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
312
|
+
// A missing heartbeat usually means the session's generation ended
|
|
313
|
+
// while its process lived on, so name the reopen command (ADR 0088).
|
|
314
|
+
// A still-live generation whose authority the ledger withholds looks
|
|
315
|
+
// the same here; name its diagnostics instead, since reopen cannot apply.
|
|
316
|
+
const { describeLiveAuthorityBlockV3, liveAuthorityBlockHintV3, liveAuthorityBlockV3 } =
|
|
317
|
+
await import("./state/live-coordination-view.ts");
|
|
318
|
+
const block = detail.includes("heartbeat_missing:")
|
|
319
|
+
? liveAuthorityBlockV3(root, owner)
|
|
320
|
+
: null;
|
|
321
|
+
const hint = block
|
|
322
|
+
? `; ${describeLiveAuthorityBlockV3(block)}; ${liveAuthorityBlockHintV3(resolveBinName(root))}`
|
|
323
|
+
: detail.includes("heartbeat_missing:")
|
|
324
|
+
? `; run \`${resolveBinName(root)} agents lifecycle active\` to open a fresh generation for this continuing session`
|
|
325
|
+
: "";
|
|
318
326
|
process.stderr.write(`agent-coord set-task: V3 authority refused (${detail})${hint}\n`);
|
|
319
327
|
return 1;
|
|
320
328
|
}
|
|
@@ -126,6 +126,7 @@ export function renderSessionContext(opts: RenderOpts): string {
|
|
|
126
126
|
if (d.missing.length > 0)
|
|
127
127
|
bits.push(`missing: ${d.missing.map((m) => m.subcommand).join(", ")}`);
|
|
128
128
|
if (d.orphans.length > 0) bits.push(`orphaned: ${d.orphans.join(", ")}`);
|
|
129
|
+
if (d.stale.length > 0) bits.push(`outdated: ${d.stale.map((m) => m.subcommand).join(", ")}`);
|
|
129
130
|
return ` - ${d.settingsFile} — ${bits.join("; ")}`;
|
|
130
131
|
});
|
|
131
132
|
messages.push(
|
|
@@ -226,3 +226,76 @@ function observedModel(generation: CoordinationGenerationViewV3): string | undef
|
|
|
226
226
|
function isLifecycleState(value: string | undefined): value is "active" | "blocked" | "done" {
|
|
227
227
|
return value === "active" || value === "blocked" || value === "done";
|
|
228
228
|
}
|
|
229
|
+
|
|
230
|
+
/** Why a still-live V3 generation has lost coordination authority. */
|
|
231
|
+
export interface LiveAuthorityBlockV3 {
|
|
232
|
+
generation_id: string;
|
|
233
|
+
scope: "generation" | "ledger";
|
|
234
|
+
diagnostics: { code: string; event_id?: string }[];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Explain an authority refusal for a generation that is still live. Callers
|
|
239
|
+
* reach this after a lookup came back empty; without it the caller sees only
|
|
240
|
+
* "missing", which reads like an ended session and points at a reopen that
|
|
241
|
+
* cannot apply. Returns null when no live generation exists or nothing blocks.
|
|
242
|
+
*/
|
|
243
|
+
export function liveAuthorityBlockV3(
|
|
244
|
+
coordRoot: string,
|
|
245
|
+
nativeInstanceId: string,
|
|
246
|
+
): LiveAuthorityBlockV3 | null {
|
|
247
|
+
let view: CoordinationViewV3;
|
|
248
|
+
try {
|
|
249
|
+
if (observeLiveEventLedgerRouteV3(coordRoot).state === "blocked") return null;
|
|
250
|
+
view = readCoordinationViewV3(coordRoot);
|
|
251
|
+
} catch {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
const generation = view.instances[liveInstanceIdV3(nativeInstanceId)];
|
|
255
|
+
if (generation?.phase !== "live" || generation.authority_eligible) return null;
|
|
256
|
+
const own = (view.diagnostics_by_generation[generation.generation_id] ?? []).filter(
|
|
257
|
+
(diagnostic) => diagnostic.authority_blocking,
|
|
258
|
+
);
|
|
259
|
+
const blocking = own.length
|
|
260
|
+
? own
|
|
261
|
+
: view.global_diagnostics.filter((diagnostic) => diagnostic.authority_blocking);
|
|
262
|
+
if (!blocking.length) return null;
|
|
263
|
+
return {
|
|
264
|
+
generation_id: generation.generation_id,
|
|
265
|
+
scope: own.length ? "generation" : "ledger",
|
|
266
|
+
diagnostics: blocking.map((diagnostic) => ({
|
|
267
|
+
code: diagnostic.source_code
|
|
268
|
+
? `${diagnostic.code}:${diagnostic.source_code}`
|
|
269
|
+
: diagnostic.code,
|
|
270
|
+
...(diagnostic.event_id ? { event_id: diagnostic.event_id } : {}),
|
|
271
|
+
})),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** One-line description of a live authority block, naming codes and events. */
|
|
276
|
+
export function describeLiveAuthorityBlockV3(block: LiveAuthorityBlockV3): string {
|
|
277
|
+
const byCode = new Map<string, string[]>();
|
|
278
|
+
for (const { code, event_id } of block.diagnostics) {
|
|
279
|
+
const events = byCode.get(code) ?? [];
|
|
280
|
+
if (event_id) events.push(event_id);
|
|
281
|
+
byCode.set(code, events);
|
|
282
|
+
}
|
|
283
|
+
const parts = [...byCode].map(([code, events]) => {
|
|
284
|
+
const shown = events.slice(0, 2).join(", ");
|
|
285
|
+
const more = events.length > 2 ? `, +${events.length - 2} more` : "";
|
|
286
|
+
return events.length ? `${code} ×${events.length} (${shown}${more})` : code;
|
|
287
|
+
});
|
|
288
|
+
const where =
|
|
289
|
+
block.scope === "generation"
|
|
290
|
+
? `the event ledger holds ${block.diagnostics.length} authority-blocking diagnostic(s) for it`
|
|
291
|
+
: "the event ledger holds authority-blocking diagnostics that block every generation";
|
|
292
|
+
return `generation ${block.generation_id} is still live, but ${where}: ${parts.join("; ")}`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Recovery pointer for a live authority block, in place of the reopen hint. */
|
|
296
|
+
export function liveAuthorityBlockHintV3(binName: string): string {
|
|
297
|
+
return (
|
|
298
|
+
`\`${binName} agents lifecycle active\` cannot help because it only reopens an ended ` +
|
|
299
|
+
`generation; \`${binName} agents health\` lists the diagnostics`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "node:fs";
|
|
13
13
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
14
14
|
import { encodeLinkSafeComponent } from "../../lib/local-file-url.ts";
|
|
15
|
-
import { findLiveTunnelForOrigin } from "../../lib/tunnel/state.ts";
|
|
15
|
+
import { findLiveTunnelForOrigin, tunnelServesPaths } from "../../lib/tunnel/state.ts";
|
|
16
16
|
import { resolveWebPort } from "../config.ts";
|
|
17
17
|
import { stateFileMode } from "../storage/modes.ts";
|
|
18
18
|
import { ARTIFACT_MANIFEST } from "./constants.ts";
|
|
@@ -326,9 +326,17 @@ function deliveryLinkBase(repoRoot: string, environment: DisplayEnvironment): st
|
|
|
326
326
|
// dashboard's own origin will answer them. A tunnel sharing this upstream
|
|
327
327
|
// port under another Host serves a different site and would 400 on /browse
|
|
328
328
|
// and /files, so fall back to the local URL rather than publish that host.
|
|
329
|
+
// The same holds for a dashboard tunnel whose path scope leaves out either
|
|
330
|
+
// route: its gate would refuse every link on the card.
|
|
331
|
+
const tunnel =
|
|
332
|
+
environment.tunnelUrl === undefined
|
|
333
|
+
? findLiveTunnelForOrigin(webPort, `localhost:${webPort}`, repoRoot)
|
|
334
|
+
: null;
|
|
329
335
|
const tunnelUrl =
|
|
330
336
|
environment.tunnelUrl === undefined
|
|
331
|
-
?
|
|
337
|
+
? tunnel && tunnelServesPaths(tunnel, ["/browse", "/files"])
|
|
338
|
+
? tunnel.url
|
|
339
|
+
: undefined
|
|
332
340
|
: environment.tunnelUrl;
|
|
333
341
|
return (tunnelUrl ?? `http://localhost:${webPort}`).replace(/\/+$/, "");
|
|
334
342
|
}
|