@chopin-ai/workspace 0.2.0 → 0.3.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.
@@ -1,11 +1,18 @@
1
1
  import { type Workspace, type WorkspaceProvider } from "./workspace.js";
2
+ export interface DirectoryWorkspaceOptions {
3
+ /**
4
+ * Directories no command in the workspace may read or write, each a real path with no symbolic link on the way,
5
+ * since that is how the sandbox sees them. Only macOS enforces this; elsewhere commands run unconfined.
6
+ */
7
+ hidden?: readonly string[];
8
+ }
2
9
  /**
3
10
  * Single-user local mode: the workspace is a directory of this machine, used as it is. There is nothing to start or
4
11
  * stop, and nothing is ever deleted: the directory is the user's own.
5
12
  */
6
13
  export declare class DirectoryWorkspaceProvider implements WorkspaceProvider {
7
14
  #private;
8
- constructor(root: string);
15
+ constructor(root: string, options?: DirectoryWorkspaceOptions);
9
16
  open(owner: string): Workspace;
10
17
  /** Nothing here can be reclaimed. */
11
18
  list(): Promise<Workspace[]>;
package/dist/directory.js CHANGED
@@ -5,8 +5,10 @@ import { carried } from "./workspace.js";
5
5
  */
6
6
  export class DirectoryWorkspaceProvider {
7
7
  #root;
8
- constructor(root) {
8
+ #hidden;
9
+ constructor(root, options = {}) {
9
10
  this.#root = root;
11
+ this.#hidden = options.hidden ?? [];
10
12
  }
11
13
  open(owner) {
12
14
  return {
@@ -21,7 +23,7 @@ export class DirectoryWorkspaceProvider {
21
23
  },
22
24
  command: (argv, env = {}) => {
23
25
  const carry = carried(argv, env);
24
- return { argv: ["sh", "-c", `cd "$1" && shift && ${carry.exec}`, "sh", this.#root, ...argv], env: carry.env };
26
+ return confined({ argv: ["sh", "-c", `cd "$1" && shift && ${carry.exec}`, "sh", this.#root, ...argv], env: carry.env }, this.#hidden);
25
27
  },
26
28
  };
27
29
  }
@@ -30,4 +32,14 @@ export class DirectoryWorkspaceProvider {
30
32
  return [];
31
33
  }
32
34
  }
35
+ /** `launch` under a macOS sandbox that denies every file operation within `hidden`, and allows all else. */
36
+ function confined(launch, hidden) {
37
+ if (hidden.length === 0 || process.platform !== "darwin")
38
+ return launch;
39
+ // Paths travel as parameters, so none is ever quoted into the profile.
40
+ const params = hidden.flatMap((path, i) => ["-D", `HIDDEN_${i}=${path}`]);
41
+ const denied = hidden.map((_, i) => `(subpath (param "HIDDEN_${i}"))`).join(" ");
42
+ const profile = `(version 1)(allow default)(deny file-read* file-write* ${denied})`;
43
+ return { argv: ["/usr/bin/sandbox-exec", ...params, "-p", profile, ...launch.argv], env: launch.env };
44
+ }
33
45
  //# sourceMappingURL=directory.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"directory.js","sourceRoot":"","sources":["../src/directory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAA0C,MAAM,gBAAgB,CAAC;AAEjF;;;GAGG;AACH,MAAM,OAAO,0BAA0B;IAC5B,KAAK,CAAS;IAEvB,YAAY,IAAY;QACtB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;IACpB,CAAC;IAED,IAAI,CAAC,KAAa;QAChB,OAAO;YACL,KAAK;YACL,IAAI,EAAE,IAAI,CAAC,KAAK;YAChB,KAAK,EAAE,KAAK,IAAI,EAAE,CAAC,SAAS;YAC5B,KAAK,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;YACrB,KAAK,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;YACrB,IAAI,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;YACpB,OAAO,EAAE,KAAK,IAAI,EAAE;gBAClB,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,CAAC,KAAK,qDAAqD,CAAC,CAAC;YACtF,CAAC;YACD,OAAO,EAAE,CAAC,IAAI,EAAE,GAAG,GAAG,EAAE,EAAE,EAAE;gBAC1B,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;gBACjC,OAAO,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,uBAAuB,KAAK,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC;YAChH,CAAC;SACF,CAAC;IACJ,CAAC;IAED,qCAAqC;IACrC,KAAK,CAAC,IAAI;QACR,OAAO,EAAE,CAAC;IACZ,CAAC;CACF"}
1
+ {"version":3,"file":"directory.js","sourceRoot":"","sources":["../src/directory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAuD,MAAM,gBAAgB,CAAC;AAU9F;;;GAGG;AACH,MAAM,OAAO,0BAA0B;IAC5B,KAAK,CAAS;IACd,OAAO,CAAoB;IAEpC,YAAY,IAAY,EAAE,OAAO,GAA8B,EAAE;QAC/D,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC;IACtC,CAAC;IAED,IAAI,CAAC,KAAa;QAChB,OAAO;YACL,KAAK;YACL,IAAI,EAAE,IAAI,CAAC,KAAK;YAChB,KAAK,EAAE,KAAK,IAAI,EAAE,CAAC,SAAS;YAC5B,KAAK,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;YACrB,KAAK,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;YACrB,IAAI,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;YACpB,OAAO,EAAE,KAAK,IAAI,EAAE;gBAClB,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,CAAC,KAAK,qDAAqD,CAAC,CAAC;YACtF,CAAC;YACD,OAAO,EAAE,CAAC,IAAI,EAAE,GAAG,GAAG,EAAE,EAAE,EAAE;gBAC1B,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;gBACjC,OAAO,QAAQ,CAAC,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,uBAAuB,KAAK,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;YACxI,CAAC;SACF,CAAC;IACJ,CAAC;IAED,qCAAqC;IACrC,KAAK,CAAC,IAAI;QACR,OAAO,EAAE,CAAC;IACZ,CAAC;CACF;AAED,4GAA4G;AAC5G,SAAS,QAAQ,CAAC,MAAc,EAAE,MAAyB;IACzD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC;IACxE,uEAAuE;IACvE,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,UAAU,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;IAC1E,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,2BAA2B,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACjF,MAAM,OAAO,GAAG,0DAA0D,MAAM,GAAG,CAAC;IACpF,OAAO,EAAE,IAAI,EAAE,CAAC,uBAAuB,EAAE,GAAG,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC;AACxG,CAAC"}
package/dist/index.d.ts CHANGED
@@ -5,3 +5,6 @@ export type { ContainerCli, ContainerWorkspaceOptions } from "./container.js";
5
5
  export { diff, run, snapshot } from "./run.js";
6
6
  export type { RunOptions, RunResult } from "./run.js";
7
7
  export { DirectoryWorkspaceProvider } from "./directory.js";
8
+ export type { DirectoryWorkspaceOptions } from "./directory.js";
9
+ export { LoopbackWorkspaceProvider } from "./loopback.js";
10
+ export type { LoopbackWorkspaceOptions } from "./loopback.js";
package/dist/index.js CHANGED
@@ -2,4 +2,5 @@ export { carried, isVariableName, reclaimIdle } from "./workspace.js";
2
2
  export { containerCli, ContainerWorkspaceProvider } from "./container.js";
3
3
  export { diff, run, snapshot } from "./run.js";
4
4
  export { DirectoryWorkspaceProvider } from "./directory.js";
5
+ export { LoopbackWorkspaceProvider } from "./loopback.js";
5
6
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAEtE,OAAO,EAAE,YAAY,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAC;AAE1E,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAE/C,OAAO,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAEtE,OAAO,EAAE,YAAY,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAC;AAE1E,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAE/C,OAAO,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAC;AAE5D,OAAO,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ // Run as `loopback-connect <port> serve --token <hex>`, where the execution daemon would be: hands the token to the
2
+ // workspace listener on that loopback port, then joins this process's standard input and output to the daemon it starts.
3
+ import { connect } from "node:net";
4
+ const [port, verb, flag, token] = process.argv.slice(2);
5
+ if (!/^\d{1,5}$/.test(port ?? "") || verb !== "serve" || flag !== "--token" || token === undefined) {
6
+ process.stderr.write("usage: loopback-connect <port> serve --token <hex>\n");
7
+ process.exit(2);
8
+ }
9
+ const link = connect(Number(port), "127.0.0.1", () => {
10
+ link.write(`${token}\n`);
11
+ process.stdin.pipe(link);
12
+ link.pipe(process.stdout);
13
+ });
14
+ link.on("error", (error) => process.stderr.write(`loopback-connect: ${error.message}\n`));
15
+ // The daemon's last answer is written out whole before this process ends.
16
+ link.on("close", (failed) => process.stdout.write("", () => process.exit(failed ? 1 : 0)));
17
+ //# sourceMappingURL=loopback-connect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loopback-connect.js","sourceRoot":"","sources":["../src/loopback-connect.ts"],"names":[],"mappings":"AAAA,oHAAoH;AACpH,yHAAyH;AACzH,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAEnC,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACxD,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,IAAI,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;IACnG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,sDAAsD,CAAC,CAAC;IAC7E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AACD,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE;IACnD,IAAI,CAAC,KAAK,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC;IACzB,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzB,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;AAC5B,CAAC,CAAC,CAAC;AACH,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qBAAqB,KAAK,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC;AAC1F,0EAA0E;AAC1E,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC"}
@@ -0,0 +1,23 @@
1
+ import type { Workspace, WorkspaceProvider } from "./workspace.js";
2
+ export interface LoopbackWorkspaceOptions {
3
+ /** The port the workspace's listener serves on the loopback address. */
4
+ port: number;
5
+ /** The persistent directory inside the workspace, where work happens. */
6
+ root: string;
7
+ /** The execution daemon as the listener starts it, the one command this workspace runs. */
8
+ daemon: string;
9
+ /** How long `start` waits for the listener to answer; a minute by default. */
10
+ startTimeoutMs?: number;
11
+ }
12
+ /**
13
+ * A workspace someone else runs beside the service on the same network, such as another container of the same pod,
14
+ * whose listener on a loopback port starts the execution daemon for each connection. Starting and stopping it is
15
+ * theirs. The daemon is all it runs: a byte stream carries no exit status, which any other command would need.
16
+ */
17
+ export declare class LoopbackWorkspaceProvider implements WorkspaceProvider {
18
+ #private;
19
+ constructor(options: LoopbackWorkspaceOptions);
20
+ open(owner: string): Workspace;
21
+ /** Nothing here can be reclaimed: whoever runs the workspace does that. */
22
+ list(): Promise<Workspace[]>;
23
+ }
@@ -0,0 +1,65 @@
1
+ import { connect } from "node:net";
2
+ import { extname } from "node:path";
3
+ import { setTimeout as sleep } from "node:timers/promises";
4
+ import { fileURLToPath } from "node:url";
5
+ /** The only address the workspace's listener is reached at: one no other machine can reach. */
6
+ const LOOPBACK = "127.0.0.1";
7
+ // The connector next to this module, run by Node as it is: compiled, or as source while testing.
8
+ const CONNECTOR = fileURLToPath(new URL(`./loopback-connect${extname(fileURLToPath(import.meta.url))}`, import.meta.url));
9
+ /**
10
+ * A workspace someone else runs beside the service on the same network, such as another container of the same pod,
11
+ * whose listener on a loopback port starts the execution daemon for each connection. Starting and stopping it is
12
+ * theirs. The daemon is all it runs: a byte stream carries no exit status, which any other command would need.
13
+ */
14
+ export class LoopbackWorkspaceProvider {
15
+ #options;
16
+ constructor(options) {
17
+ if (!Number.isInteger(options.port) || options.port < 1 || options.port > 65_535)
18
+ throw new Error(`${options.port} is no port`);
19
+ this.#options = options;
20
+ }
21
+ open(owner) {
22
+ const { port, root, daemon, startTimeoutMs = 60_000 } = this.#options;
23
+ const at = `${LOOPBACK}:${port}`;
24
+ const managed = async () => {
25
+ throw new Error(`the workspace at ${at} is started and stopped by whoever runs it`);
26
+ };
27
+ return {
28
+ owner,
29
+ root,
30
+ state: async () => ((await answers(port)) ? "running" : "stopped"),
31
+ start: async () => {
32
+ const deadline = Date.now() + startTimeoutMs;
33
+ while (!(await answers(port))) {
34
+ if (Date.now() >= deadline)
35
+ throw new Error(`no workspace listener answered at ${at}`);
36
+ await sleep(250);
37
+ }
38
+ },
39
+ pause: managed,
40
+ stop: managed,
41
+ destroy: managed,
42
+ command: (argv, env = {}) => {
43
+ if (argv.length !== 1 || argv[0] !== daemon || Object.keys(env).length > 0) {
44
+ throw new Error(`the workspace at ${at} runs only its execution daemon, ${daemon}`);
45
+ }
46
+ return { argv: [process.execPath, CONNECTOR, String(port)], env: {} };
47
+ },
48
+ };
49
+ }
50
+ /** Nothing here can be reclaimed: whoever runs the workspace does that. */
51
+ async list() {
52
+ return [];
53
+ }
54
+ }
55
+ /** Whether a listener accepts a connection on the loopback `port` now. */
56
+ function answers(port) {
57
+ return new Promise((resolve) => {
58
+ const probe = connect(port, LOOPBACK, () => {
59
+ probe.destroy();
60
+ resolve(true);
61
+ });
62
+ probe.on("error", () => resolve(false));
63
+ });
64
+ }
65
+ //# sourceMappingURL=loopback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loopback.js","sourceRoot":"","sources":["../src/loopback.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ #!/bin/sh
2
+ # Serves this workspace to an agent service beside it on the same network, as LoopbackWorkspaceProvider reaches it:
3
+ # loopback-listener.sh <port> <daemon>
4
+ # Each connection starts the execution daemon with the connection as its standard input and output, under the token
5
+ # the connection's first line carries; one that sends nothing was only checking the listener is up. It listens on the
6
+ # loopback address alone, which nothing outside this network can reach. The daemon's path, and this script's, have no
7
+ # space, colon or comma, which the relay's addresses would split on.
8
+ set -eu
9
+ if [ "${1-}" = --connection ]; then
10
+ read -r token || exit 0
11
+ case $token in '' | *[!0-9a-f]*) exit 2 ;; esac
12
+ exec "$2" serve --token "$token"
13
+ fi
14
+ case ${1-}:${2-} in '' | :* | *[!0-9]*:* | *:) echo "usage: loopback-listener.sh <port> <daemon>" >&2; exit 2 ;; esac
15
+ # Once a connection stops sending, the daemon is given time to answer what it was asked; it ends sooner by itself.
16
+ exec socat -t 10 "TCP-LISTEN:$1,bind=127.0.0.1,reuseaddr,fork" "EXEC:$0 --connection $2"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chopin-ai/workspace",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Per-user workspace lifecycle interface with directory and container implementations",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -19,6 +19,7 @@
19
19
  }
20
20
  },
21
21
  "files": [
22
- "dist"
22
+ "dist",
23
+ "loopback-listener.sh"
23
24
  ]
24
25
  }