humanish 0.15.3 → 0.17.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.
Files changed (48) hide show
  1. package/README.md +49 -6
  2. package/dist/concurrent-shared-world-lab.js +20 -7
  3. package/dist/concurrent-shared-world-lab.js.map +1 -1
  4. package/dist/cua-actor-lab.d.ts +101 -4
  5. package/dist/cua-actor-lab.js +456 -77
  6. package/dist/cua-actor-lab.js.map +1 -1
  7. package/dist/device-presets.d.ts +4 -4
  8. package/dist/device-presets.js +5 -5
  9. package/dist/device-presets.js.map +1 -1
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/lab-config.d.ts +2 -2
  13. package/dist/observer-assets.js +92 -15
  14. package/dist/observer-assets.js.map +1 -1
  15. package/dist/observer-auth.d.ts +21 -0
  16. package/dist/observer-auth.js +92 -0
  17. package/dist/observer-auth.js.map +1 -0
  18. package/dist/observer-library.d.ts +19 -0
  19. package/dist/observer-library.js +189 -0
  20. package/dist/observer-library.js.map +1 -0
  21. package/dist/observer-serve.d.ts +111 -0
  22. package/dist/observer-serve.js +369 -0
  23. package/dist/observer-serve.js.map +1 -0
  24. package/dist/observer.d.ts +31 -0
  25. package/dist/observer.js +10 -5
  26. package/dist/observer.js.map +1 -1
  27. package/dist/oss-meta-lab.js +2 -4
  28. package/dist/oss-meta-lab.js.map +1 -1
  29. package/dist/program.js +242 -0
  30. package/dist/program.js.map +1 -1
  31. package/dist/run.d.ts +38 -0
  32. package/dist/run.js +71 -1
  33. package/dist/run.js.map +1 -1
  34. package/dist/serve-tunnel.d.ts +16 -0
  35. package/dist/serve-tunnel.js +104 -0
  36. package/dist/serve-tunnel.js.map +1 -0
  37. package/dist/shared-world-lab.d.ts +20 -1
  38. package/dist/shared-world-lab.js +197 -23
  39. package/dist/shared-world-lab.js.map +1 -1
  40. package/docs/architecture/observer.md +7 -0
  41. package/docs/architecture/serve.md +140 -0
  42. package/docs/assets/humanish-drawdb-hero.png +0 -0
  43. package/docs/contracts/run-bundle.md +19 -0
  44. package/docs/contracts/schemas.md +23 -1
  45. package/docs/goals/current.md +4 -3
  46. package/docs/principles/invariants-and-defaults.md +1 -0
  47. package/docs/ramp/README.md +1 -1
  48. package/package.json +1 -1
package/dist/program.js CHANGED
@@ -13,6 +13,8 @@ import { runLabPreflight } from "./lab-preflight.js";
13
13
  import { runLab, resolveLabDryRun, selectLabBackend } from "./lab-engine.js";
14
14
  import { openTarget, renderObserver, serveObserver } from "./observer.js";
15
15
  import { serveObserverStatic } from "./observer-static.js";
16
+ import { SERVE_SCHEMA, parsePublicOrigin, serveObserverLibrary } from "./observer-serve.js";
17
+ import { ServeTunnelError, startNgrokTunnel } from "./serve-tunnel.js";
16
18
  import { DEFAULT_OSS_REPOS, runOssLab } from "./oss-lab.js";
17
19
  import { cleanupOssMetaLabSandboxes, cleanupStaleOssMetaLabSandboxes, runOssMetaLab, startOssMetaLabLiveRefresh } from "./oss-meta-lab.js";
18
20
  import { cleanupRun, doctor, listRuns, readReview, runDryRun, verifyRun } from "./run.js";
@@ -163,6 +165,7 @@ export function createProgram(io = {}) {
163
165
  registerRunsCommand(program, cliIo);
164
166
  registerWatchCommand(program, cliIo);
165
167
  registerObserveCommand(program, cliIo);
168
+ registerServeCommand(program, cliIo);
166
169
  registerCodexCommands(program, cliIo);
167
170
  registerLabCommands(program, cliIo);
168
171
  registerFeedbackCommands(program, cliIo);
@@ -788,6 +791,245 @@ async function serveObserveUntilSignal(io, server, options) {
788
791
  }
789
792
  });
790
793
  }
794
+ function registerServeCommand(parent, io) {
795
+ parent
796
+ .command("serve")
797
+ .description("Serve the local run library over loopback http, with optional capability-link exposure.")
798
+ .summary("Serve the run library; optional capability-link exposure.")
799
+ .option("--cwd <path>", "Target project directory.", ".")
800
+ .option("--port <port>", "Loopback port to bind on 127.0.0.1. Defaults to an ephemeral port.", "0")
801
+ .option("--run <id>", "Land on this run id (or latest) instead of the library index.")
802
+ .option("--safe", "Serve only runs whose verify shareSafety is share_ready; everything else is absent (fail-closed).")
803
+ .option("--expose", "Declare exposure intent: enables the capability-link auth gate on every request and prints the secret link once. Requires --tunnel or --public-url.")
804
+ .addOption(new Option("--auth <mode>", "Auth mode under --expose: capability link, or none (requires --safe).").choices(["link", "none"]).default("link"))
805
+ .option("--ttl <minutes>", "Capability session lifetime in minutes under --expose.", "720")
806
+ .addOption(new Option("--tunnel <provider>", "Spawn the OPTIONAL external tunnel binary against the loopback port.").choices(["ngrok"]))
807
+ .option("--tunnel-domain <domain>", "Reserved domain passed to ngrok as --url (e.g. observer.example.dev). Requires --tunnel.")
808
+ .option("--public-url <origin>", "Declared public origin when you run your own tunnel/proxy (e.g. https://observer.example.dev). Requires --expose; never affects binding.")
809
+ .option("--open", "Open the library in the default browser.")
810
+ .option("--no-open", "Serve without opening a browser.")
811
+ .option("--json", JSON_OPTION_DESCRIPTION)
812
+ .addHelpText("after", [
813
+ "",
814
+ "Happy path:",
815
+ " humanish serve",
816
+ " humanish serve --expose --tunnel ngrok --tunnel-domain observer.example.dev",
817
+ " humanish serve --safe --expose --auth none --tunnel ngrok",
818
+ "",
819
+ "Agent/CI path:",
820
+ " humanish serve --json --no-open",
821
+ "",
822
+ "The server always binds 127.0.0.1; exposure only ever happens through a tunnel",
823
+ "forwarding to the loopback port. The capability link is minted fresh per process:",
824
+ "Ctrl-C revokes the link and every session. Live desktop stream URLs are never",
825
+ "served in any mode; remote viewers see persisted evidence (screenshots, events).",
826
+ "--safe composes with the capability link for defense in depth."
827
+ ].join("\n"))
828
+ .action(async (options, command) => {
829
+ const wantsMachine = wantsJson(command);
830
+ const fail = (code, message) => {
831
+ const result = {
832
+ schema: SERVE_SCHEMA,
833
+ ok: false,
834
+ cwd: options.cwd,
835
+ mode: "loopback",
836
+ safe: options.safe === true,
837
+ host: "127.0.0.1",
838
+ runsListed: 0,
839
+ warnings: [],
840
+ error: { code, message }
841
+ };
842
+ writeResult(command, io, result, formatServeHuman);
843
+ io.setExitCode(2);
844
+ };
845
+ const port = parseObserverPort(options.port);
846
+ if (port === null) {
847
+ fail("HUMANISH_INVALID_PORT", "--port must be an integer between 0 and 65535.");
848
+ return;
849
+ }
850
+ const ttlMinutes = /^\d+$/.test(options.ttl) ? Number.parseInt(options.ttl, 10) : null;
851
+ if (ttlMinutes === null || ttlMinutes < 1 || ttlMinutes > 10_080) {
852
+ fail("HUMANISH_SERVE_INVALID_TTL", "--ttl must be an integer between 1 and 10080 (minutes).");
853
+ return;
854
+ }
855
+ const authExplicit = command.getOptionValueSource("auth") === "cli";
856
+ const ttlExplicit = command.getOptionValueSource("ttl") === "cli";
857
+ if (authExplicit && options.expose !== true) {
858
+ fail("HUMANISH_SERVE_OPTION_CONFLICT", "--auth only applies with --expose.");
859
+ return;
860
+ }
861
+ if (ttlExplicit && options.expose !== true) {
862
+ fail("HUMANISH_SERVE_OPTION_CONFLICT", "--ttl only applies with --expose.");
863
+ return;
864
+ }
865
+ if (options.auth === "none" && options.safe !== true) {
866
+ fail("HUMANISH_SERVE_OPEN_REQUIRES_SAFE", "--auth none serves without a secret; that is publishing, so it requires --safe (share_ready runs only).");
867
+ return;
868
+ }
869
+ if (options.tunnel && options.expose !== true) {
870
+ fail("HUMANISH_SERVE_TUNNEL_REQUIRES_EXPOSE", "--tunnel exposes the library; declare that intent with --expose.");
871
+ return;
872
+ }
873
+ if (options.publicUrl !== undefined && options.expose !== true) {
874
+ fail("HUMANISH_SERVE_OPTION_CONFLICT", "--public-url only applies with --expose.");
875
+ return;
876
+ }
877
+ if (options.tunnelDomain !== undefined && !options.tunnel) {
878
+ fail("HUMANISH_SERVE_OPTION_CONFLICT", "--tunnel-domain requires --tunnel.");
879
+ return;
880
+ }
881
+ if (options.tunnel && options.publicUrl !== undefined) {
882
+ fail("HUMANISH_SERVE_OPTION_CONFLICT", "Use either --tunnel or --public-url as the public origin, not both.");
883
+ return;
884
+ }
885
+ if (options.expose === true && !options.tunnel && options.publicUrl === undefined) {
886
+ fail("HUMANISH_SERVE_EXPOSE_REQUIRES_ORIGIN", "--expose needs a declared public origin: pass --tunnel ngrok or --public-url <origin>. The Host allowlist stays strict in every mode.");
887
+ return;
888
+ }
889
+ const declaredOrigin = options.publicUrl !== undefined ? parsePublicOrigin(options.publicUrl) : null;
890
+ if (options.publicUrl !== undefined && !declaredOrigin) {
891
+ fail("HUMANISH_SERVE_OPTION_CONFLICT", "--public-url must be an http(s) origin like https://observer.example.dev.");
892
+ return;
893
+ }
894
+ const started = await serveObserverLibrary(options.cwd, {
895
+ port,
896
+ safe: options.safe === true,
897
+ expose: options.expose === true,
898
+ authMode: options.auth,
899
+ ttlMinutes,
900
+ ...(declaredOrigin ? { publicOrigin: declaredOrigin.origin } : {}),
901
+ ...(options.run ? { entryRunId: options.run } : {})
902
+ });
903
+ if (!started.ok) {
904
+ fail(started.error.code, started.error.message);
905
+ return;
906
+ }
907
+ const server = started.server;
908
+ let tunnel;
909
+ if (options.tunnel) {
910
+ try {
911
+ tunnel = await startNgrokTunnel({
912
+ port: server.port,
913
+ ...(options.tunnelDomain ? { domain: options.tunnelDomain } : {})
914
+ });
915
+ }
916
+ catch (error) {
917
+ await server.close();
918
+ if (error instanceof ServeTunnelError) {
919
+ fail(error.code, error.message);
920
+ }
921
+ else {
922
+ fail("HUMANISH_SERVE_TUNNEL_START_FAILED", `Tunnel startup failed: ${error instanceof Error ? error.message : String(error)}`);
923
+ }
924
+ return;
925
+ }
926
+ // Declares the tunnel's https origin: extends the Host allowlist and
927
+ // marks minted cookies Secure (every ngrok tunnel is https).
928
+ server.addPublicOrigin(tunnel.url);
929
+ }
930
+ const publicUrl = tunnel ? tunnel.url.replace(/\/$/, "") : declaredOrigin?.origin;
931
+ const capabilityUrl = server.capabilityToken
932
+ ? `${server.url.replace(/\/$/, "")}/_humanish/auth/${server.capabilityToken}`
933
+ : undefined;
934
+ const publicCapabilityUrl = server.capabilityToken && publicUrl
935
+ ? `${publicUrl}/_humanish/auth/${server.capabilityToken}`
936
+ : undefined;
937
+ const warnings = [];
938
+ if (server.mode === "capability-link" && options.safe !== true) {
939
+ warnings.push(`capability link grants read access to all ${server.runsListed} local runs, including any not verified share_ready (local_only raw screenshots, blocked bundles); anyone holding the link can view them until this process exits; restart rotates the link; add --safe to restrict to share_ready`);
940
+ }
941
+ if (server.mode === "capability-link" && options.safe === true) {
942
+ warnings.push(`capability link grants read access to ${server.shareReadyCount ?? 0} share_ready runs; non-share_ready runs are absent even to link holders`);
943
+ }
944
+ if (server.mode === "share-safe-open") {
945
+ warnings.push(`serving ${server.shareReadyCount ?? 0} share_ready runs to anyone who can reach ${publicUrl}; non-share_ready runs are absent and their URLs 404`);
946
+ }
947
+ warnings.push("live desktop stream URLs are never served; remote viewers see persisted evidence (screenshots, events, terminal tails) only");
948
+ // Auto-open is suppressed under --expose so the capability token is not
949
+ // placed into a local opener process's argv (readable via `ps`) without
950
+ // the operator asking — the exposure target is a remote device anyway.
951
+ // Explicit --open still honors intent and opens the capability URL.
952
+ const shouldOpen = options.open === false
953
+ ? false
954
+ : options.open === true
955
+ ? true
956
+ : !wantsMachine && process.stdout.isTTY === true && options.expose !== true;
957
+ const openResult = shouldOpen ? openTarget(capabilityUrl ?? server.url) : { opened: false };
958
+ if (openResult.warning) {
959
+ warnings.push(openResult.warning);
960
+ }
961
+ const result = {
962
+ schema: SERVE_SCHEMA,
963
+ ok: true,
964
+ cwd: options.cwd,
965
+ mode: server.mode,
966
+ safe: options.safe === true,
967
+ host: "127.0.0.1",
968
+ port: server.port,
969
+ url: server.url,
970
+ ...(capabilityUrl ? { capabilityUrl } : {}),
971
+ ...(publicUrl ? { publicUrl } : {}),
972
+ ...(publicCapabilityUrl ? { publicCapabilityUrl } : {}),
973
+ ...(tunnel ? { tunnel: { provider: "ngrok", url: tunnel.url } } : {}),
974
+ ...(server.mode === "capability-link" ? { ttlMinutes } : {}),
975
+ runsListed: server.runsListed,
976
+ ...(server.shareReadyCount !== undefined ? { shareReadyCount: server.shareReadyCount } : {}),
977
+ ...(server.entryRunId ? { entryRunId: server.entryRunId } : {}),
978
+ opened: openResult.opened,
979
+ ...(openResult.command ? { openCommand: openResult.command } : {}),
980
+ warnings
981
+ };
982
+ writeResult(command, io, result, formatServeHuman);
983
+ io.setExitCode(0);
984
+ await serveObserveUntilSignal(io, {
985
+ url: server.url,
986
+ close: async () => {
987
+ if (tunnel) {
988
+ await tunnel.close();
989
+ }
990
+ await server.close();
991
+ }
992
+ }, { json: wantsMachine });
993
+ });
994
+ }
995
+ function formatServeHuman(result) {
996
+ if (!result.ok) {
997
+ return [
998
+ "humanish serve failed",
999
+ ...(result.error ? [`error: ${result.error.code} ${result.error.message}`] : []),
1000
+ ...result.warnings.map((warning) => `warning: ${warning}`)
1001
+ ].join("\n") + "\n";
1002
+ }
1003
+ const modeSuffix = result.safe ? " (share_ready only)" : "";
1004
+ const lines = [
1005
+ "humanish serve",
1006
+ `mode: ${result.mode}${modeSuffix}`,
1007
+ `library: ${result.url ?? ""}`,
1008
+ `runs: ${result.runsListed}`
1009
+ ];
1010
+ if (result.capabilityUrl) {
1011
+ lines.push("SECRET LINK (anyone holding it can read the library until Ctrl-C):");
1012
+ lines.push(` ${result.capabilityUrl}`);
1013
+ if (result.publicCapabilityUrl) {
1014
+ lines.push(` ${result.publicCapabilityUrl}`);
1015
+ }
1016
+ lines.push("revocation: Ctrl-C revokes the link and all sessions; restarting mints a new link");
1017
+ }
1018
+ else if (result.publicUrl) {
1019
+ lines.push(`public: ${result.publicUrl}`);
1020
+ }
1021
+ if (result.tunnel) {
1022
+ lines.push(`tunnel: ${result.tunnel.provider} ${result.tunnel.url}`);
1023
+ }
1024
+ if (result.entryRunId) {
1025
+ lines.push(`entry: ${result.entryRunId}`);
1026
+ }
1027
+ lines.push(`opened: ${result.opened === true ? "yes" : "no"}`);
1028
+ for (const warning of result.warnings) {
1029
+ lines.push(`warning: ${warning}`);
1030
+ }
1031
+ return lines.join("\n") + "\n";
1032
+ }
791
1033
  function registerFeedbackCommands(parent, io) {
792
1034
  const feedback = parent
793
1035
  .command("feedback")