@nosana/node 1.1.63-rc → 1.1.64-rc

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 (82) hide show
  1. package/README.md +0 -1
  2. package/dist/.env +2 -4
  3. package/dist/.env.dev +1 -1
  4. package/dist/.env.prd +1 -1
  5. package/dist/package.json +3 -2
  6. package/dist/src/NodeManager/configs/NodeConfigs.d.ts +1 -0
  7. package/dist/src/NodeManager/configs/NodeConfigs.js +4 -1
  8. package/dist/src/NodeManager/configs/configs.d.ts +0 -3
  9. package/dist/src/NodeManager/configs/configs.js +2 -5
  10. package/dist/src/NodeManager/db/index.d.ts +23 -0
  11. package/dist/src/NodeManager/index.js +9 -4
  12. package/dist/src/NodeManager/monitoring/reportPodmanDiagnostics.d.ts +2 -3
  13. package/dist/src/NodeManager/monitoring/reportPodmanDiagnostics.js +8 -12
  14. package/dist/src/NodeManager/node/Node.d.ts +0 -3
  15. package/dist/src/NodeManager/node/Node.js +0 -32
  16. package/dist/src/NodeManager/node/api/ApiHandler.js +16 -3
  17. package/dist/src/NodeManager/node/api/middlewares/index.d.ts +0 -2
  18. package/dist/src/NodeManager/node/api/middlewares/index.js +0 -2
  19. package/dist/src/NodeManager/node/api/middlewares/rest/verifyJobOwnerSignatureMiddleware.js +1 -0
  20. package/dist/src/NodeManager/node/api/middlewares/ws/verifyWSJobOwnerSignatureMiddleware.js +1 -0
  21. package/dist/src/NodeManager/node/api/routes/get/node-info.js +9 -7
  22. package/dist/src/NodeManager/node/api/routes/post/index.d.ts +1 -0
  23. package/dist/src/NodeManager/node/api/routes/post/index.js +1 -0
  24. package/dist/src/NodeManager/node/api/routes/post/ssh-authorize.d.ts +17 -0
  25. package/dist/src/NodeManager/node/api/routes/post/ssh-authorize.js +53 -0
  26. package/dist/src/NodeManager/node/api/routes/wss/index.d.ts +2 -0
  27. package/dist/src/NodeManager/node/api/routes/wss/index.js +2 -0
  28. package/dist/src/NodeManager/node/api/routes/wss/log.js +2 -1
  29. package/dist/src/NodeManager/node/api/routes/wss/terminal/terminal.d.ts +21 -0
  30. package/dist/src/NodeManager/node/api/routes/wss/terminal/terminal.js +165 -0
  31. package/dist/src/NodeManager/node/api/routes/wss/terminal/terminalAuthorization.d.ts +18 -0
  32. package/dist/src/NodeManager/node/api/routes/wss/terminal/terminalAuthorization.js +61 -0
  33. package/dist/src/NodeManager/node/market/hostManager.d.ts +3 -3
  34. package/dist/src/NodeManager/node/resource/helpers/isAuthGatedResource.d.ts +9 -0
  35. package/dist/src/NodeManager/node/resource/helpers/isAuthGatedResource.js +11 -0
  36. package/dist/src/NodeManager/node/resource/image/imageManager.d.ts +14 -1
  37. package/dist/src/NodeManager/node/resource/image/imageManager.js +30 -4
  38. package/dist/src/NodeManager/node/resource/resourceManager.d.ts +1 -1
  39. package/dist/src/NodeManager/node/resource/resourceManager.js +21 -5
  40. package/dist/src/NodeManager/node/resource/volume/volumeManager.d.ts +31 -4
  41. package/dist/src/NodeManager/node/resource/volume/volumeManager.js +69 -12
  42. package/dist/src/NodeManager/node/task/TaskManagerRegistry.d.ts +0 -8
  43. package/dist/src/NodeManager/node/task/TaskManagerRegistry.js +0 -22
  44. package/dist/src/NodeManager/provider/Provider.d.ts +30 -14
  45. package/dist/src/NodeManager/provider/Provider.js +157 -116
  46. package/dist/src/NodeManager/provider/SshAccessManager.d.ts +49 -0
  47. package/dist/src/NodeManager/provider/SshAccessManager.js +148 -0
  48. package/dist/src/NodeManager/provider/containerOrchestration/docker/createDockerRunOptions.d.ts +1 -1
  49. package/dist/src/NodeManager/provider/containerOrchestration/docker/createDockerRunOptions.js +6 -3
  50. package/dist/src/NodeManager/provider/containerOrchestration/docker/index.d.ts +5 -1
  51. package/dist/src/NodeManager/provider/containerOrchestration/docker/index.js +40 -2
  52. package/dist/src/NodeManager/provider/containerOrchestration/interface.d.ts +28 -1
  53. package/dist/src/NodeManager/provider/containerOrchestration/labels.d.ts +10 -0
  54. package/dist/src/NodeManager/provider/containerOrchestration/labels.js +12 -0
  55. package/dist/src/NodeManager/provider/containerOrchestration/podman/index.js +14 -9
  56. package/dist/src/NodeManager/provider/containerOrchestration/podman/utils/createPodmanRunOptions.d.ts +39 -0
  57. package/dist/src/NodeManager/provider/containerOrchestration/podman/utils/createPodmanRunOptions.js +16 -2
  58. package/dist/src/NodeManager/provider/sshSidecar.d.ts +66 -0
  59. package/dist/src/NodeManager/provider/sshSidecar.js +176 -0
  60. package/dist/src/NodeManager/provider/webTerminal.d.ts +24 -0
  61. package/dist/src/NodeManager/provider/webTerminal.js +98 -0
  62. package/dist/src/NodeManager/repository/NodeRepository.d.ts +5 -1
  63. package/dist/src/NodeManager/repository/NodeRepository.js +20 -0
  64. package/dist/src/cli/commands/prune/action.js +1 -2
  65. package/dist/src/cli/commands/run/action.js +1 -14
  66. package/dist/src/cli/commands/run/command.js +1 -2
  67. package/dist/src/cli/commands/sharedOptions/index.d.ts +0 -1
  68. package/dist/src/cli/commands/sharedOptions/index.js +0 -1
  69. package/dist/src/cli/commands/start/action.js +1 -2
  70. package/dist/src/cli/commands/start/command.js +1 -2
  71. package/npm-shrinkwrap.json +28 -2
  72. package/package.json +3 -2
  73. package/dist/src/NodeManager/node/api/middlewares/ws/verifyWSMiddleware.d.ts +0 -3
  74. package/dist/src/NodeManager/node/api/middlewares/ws/verifyWSMiddleware.js +0 -20
  75. package/dist/src/NodeManager/node/api/middlewares/ws/verifyWSNodeOrJobOwnerSignatureMiddleware.d.ts +0 -3
  76. package/dist/src/NodeManager/node/api/middlewares/ws/verifyWSNodeOrJobOwnerSignatureMiddleware.js +0 -33
  77. package/dist/src/NodeManager/provider/gpuRuntime.d.ts +0 -13
  78. package/dist/src/NodeManager/provider/gpuRuntime.js +0 -27
  79. package/dist/src/NodeManager/provider/podmanManager/PodmanManager.d.ts +0 -72
  80. package/dist/src/NodeManager/provider/podmanManager/PodmanManager.js +0 -238
  81. package/dist/src/cli/commands/sharedOptions/--managed-podman.d.ts +0 -16
  82. package/dist/src/cli/commands/sharedOptions/--managed-podman.js +0 -25
package/README.md CHANGED
@@ -80,7 +80,6 @@ Options:
80
80
  --provider <provider> provider used to run the job (choices: "docker", "podman", default: "podman")
81
81
  -w, --wallet <wallet> path to wallet private key (default: "~/.nosana/nosana_key.json")
82
82
  --docker, --podman <URI> Podman/Docker connection URI (default: "~/.nosana/podman/podman.sock")
83
- --managed-podman Run jobs in a podman container the node creates over the docker socket given with --docker, and recreates when its GPU mounts go stale
84
83
  -h, --help display help for command
85
84
  ```
86
85
 
package/dist/.env CHANGED
@@ -1,8 +1,6 @@
1
1
  BACKEND_SOLANA_ADDRESS=7rFPFnxjXHC2sfDy3qrDa9pEb4j49oivMuV7e8sYDPmB
2
- FRPC_CONTAINER_IMAGE=registry.hub.docker.com/nosana/frpc:v1.0.44
3
- FRP_SERVER_IMAGE=registry.hub.docker.com/nosana/frpc:v1.0.44
4
- PODMAN_CONTAINER_IMAGE=docker.io/nosana/podman:v1.1.2
2
+ FRP_SERVER_IMAGE=registry.hub.docker.com/nosana/frpc:v1.0.45
5
3
  FRP_SERVER_PORT=7000
6
4
  MIN_DISK_SPACE=25
7
5
  SIGN_MESSAGE="Hello Nosana Node!"
8
- API_PORT=8123
6
+ API_PORT=8123
package/dist/.env.dev CHANGED
@@ -1,4 +1,4 @@
1
1
  HOST_MANAGER_BASE_URL=https://host-manager.k8s.dev.nos.ci
2
2
  EXPLORER_URL=https://devnet.nosana.io
3
3
  FRP_SERVER_ADDRESS=node.k8s.dev.nos.ci
4
- MIN_DISK_SPACE=20
4
+ MIN_DISK_SPACE=20
package/dist/.env.prd CHANGED
@@ -1,3 +1,3 @@
1
1
  HOST_MANAGER_BASE_URL=https://host-manager.k8s.prd.nos.ci
2
2
  EXPLORER_URL=https://explore.nosana.com
3
- FRP_SERVER_ADDRESS=node.k8s.prd.nos.ci
3
+ FRP_SERVER_ADDRESS=node.k8s.prd.nos.ci
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nosana/node",
3
- "version": "1.1.63-rc",
3
+ "version": "1.1.64-rc",
4
4
  "description": "",
5
5
  "bin": {
6
6
  "nosana-node": "./dist/src/index.js"
@@ -15,7 +15,7 @@
15
15
  "generate": "npm run generate:host-manager",
16
16
  "generate:host-manager": "openapi-typescript https://host-manager.k8s.dev.nos.ci/swagger/json -o ./src/NodeManager/clients/hostManager/schema.d.ts",
17
17
  "test": "APP_ENV=development DOCKER_CERT_PATH= vitest run",
18
- "ci:test": "APP_ENV=development DOCKER_CERT_PATH= vitest run --coverage --reporter=junit --outputFile=./test-report.xml",
18
+ "ci:test": "APP_ENV=development DOCKER_CERT_PATH= vitest run --coverage --reporter=verbose --reporter=junit --outputFile.junit=./test-report.xml",
19
19
  "test:watch": "APP_ENV=development DOCKER_CERT_PATH= vitest",
20
20
  "test:coverage": "APP_ENV=development DOCKER_CERT_PATH= vitest run --coverage",
21
21
  "publish:next": "npm run build && npm publish --tag=next --access public",
@@ -74,6 +74,7 @@
74
74
  "@inquirer/prompts": "^4.3.0",
75
75
  "@nosana/ipfs": "^2.2.6",
76
76
  "@nosana/sdk": "^0.4.87",
77
+ "@nosana/ssh": "^2.13.1",
77
78
  "@solana/web3.js": "^1.78.0",
78
79
  "bn.js": "^5.2.1",
79
80
  "chalk": "^5.3.0",
@@ -3,6 +3,7 @@ export declare class NodeConfigs {
3
3
  constructor(options: {
4
4
  [key: string]: any;
5
5
  });
6
+ getNetwork(): 'devnet' | 'mainnet';
6
7
  loadVariablesToEnv(): void;
7
8
  }
8
9
  export declare class NodeConfigsSingleton {
@@ -5,8 +5,11 @@ export class NodeConfigs {
5
5
  constructor(options) {
6
6
  this.options = options;
7
7
  }
8
+ getNetwork() {
9
+ return this.options.network ?? 'mainnet';
10
+ }
8
11
  loadVariablesToEnv() {
9
- const network = this.options.network ? this.options.network : 'mainnet';
12
+ const network = this.getNetwork();
10
13
  const env = network === 'mainnet' ? 'prd' : 'dev';
11
14
  const modulePath = dirname(fileURLToPath(import.meta.url));
12
15
  // Load .env.${env} first (higher priority)
@@ -9,9 +9,6 @@ export type configType = {
9
9
  containerImage: string;
10
10
  nodeInternalHostName: string;
11
11
  };
12
- podman: {
13
- containerImage: string;
14
- };
15
12
  api: {
16
13
  port: number;
17
14
  };
@@ -1,7 +1,7 @@
1
1
  import { NodeConfigsSingleton } from './NodeConfigs.js';
2
2
  import { loadConfigurationValue } from '../utils/utils.js';
3
3
  export const configs = (options) => {
4
- NodeConfigsSingleton.getInstance(options);
4
+ const nodeConfigs = NodeConfigsSingleton.getInstance(options);
5
5
  return {
6
6
  isNodeRun: options?.isNodeRun,
7
7
  hostManagerBaseUrl: loadConfigurationValue('HOST_MANAGER_BASE_URL'),
@@ -13,13 +13,10 @@ export const configs = (options) => {
13
13
  containerImage: loadConfigurationValue('FRP_SERVER_IMAGE'),
14
14
  nodeInternalHostName: loadConfigurationValue('FRP_NODE_INTERNAL_HOSTNAME', "nosana-node"),
15
15
  },
16
- podman: {
17
- containerImage: loadConfigurationValue('PODMAN_CONTAINER_IMAGE'),
18
- },
19
16
  api: {
20
17
  port: parseInt(loadConfigurationValue('API_PORT')),
21
18
  },
22
19
  minDiskSpace: parseInt(loadConfigurationValue('MIN_DISK_SPACE')),
23
- network: options?.network ?? 'mainnet',
20
+ network: options?.network ?? nodeConfigs.getNetwork(),
24
21
  };
25
22
  };
@@ -1,7 +1,14 @@
1
1
  import { LowSync } from 'lowdb/lib';
2
2
  import { Flow } from '@nosana/sdk';
3
3
  import { CudaCheckSuccessResponse } from '../types/index.js';
4
+ export type SshKeyGrant = {
5
+ sshPublicKey: string;
6
+ expiresAt?: string;
7
+ };
4
8
  export type NodeDb = {
9
+ sshKeyGrants?: Record<string, SshKeyGrant[]>;
10
+ /** One SSH host key (PKCS#8 PEM) per job, shared by all of its sidecars. */
11
+ sshHostKeys?: Record<string, string>;
5
12
  flows: {
6
13
  [key: string]: Flow;
7
14
  };
@@ -40,6 +47,22 @@ export type ResourceHistory = {
40
47
  usage: number;
41
48
  required: boolean;
42
49
  isPrivate?: boolean;
50
+ /**
51
+ * The project a resource is scoped to when it must not be shared freely
52
+ * across projects. Absent on shared, public resources.
53
+ *
54
+ * For a volume it is set once the resource is mounted writable (`allowWrite`)
55
+ * or fetched from an auth-gated remote: a writable mount can leave the job's
56
+ * own bytes behind, and an auth-gated resource is private to the credentials
57
+ * that fetched it, so the volume is keyed by project and never reused by
58
+ * another — a different project falls through to a clean copy of its own.
59
+ *
60
+ * For a private image it is the project the image was last pulled for. The
61
+ * daemon holds one copy per image reference, so this gates reuse rather than
62
+ * separating storage: a job from a different project must re-authenticate
63
+ * (forcing a pull) before it may reuse the cached copy.
64
+ */
65
+ project?: string;
43
66
  };
44
67
  export type VolumeResource = ResourceHistory & {
45
68
  volume: string;
@@ -64,7 +64,7 @@ export default class NodeManager {
64
64
  * collect the diagnostics the podman container leaves in the shared
65
65
  * directory, which has no other route off the node
66
66
  */
67
- reportPodmanDiagnostics(this.configLocation, (event) => this.node.podmanEvent(event));
67
+ reportPodmanDiagnostics(this.configLocation);
68
68
  /**
69
69
  * start
70
70
  *
@@ -109,7 +109,10 @@ export default class NodeManager {
109
109
  * request a market to join from the host manager,
110
110
  * we can pass a requested market or let the host manager recommend us one
111
111
  */
112
- const market = await this.node.requestMarket(marketArg);
112
+ let market;
113
+ if (!marketArg) {
114
+ market = await this.node.requestMarket();
115
+ }
113
116
  /**
114
117
  * healthcheck
115
118
  *
@@ -129,7 +132,9 @@ export default class NodeManager {
129
132
  *
130
133
  * this sets up everything need, downloads needed resources ...
131
134
  */
132
- await this.node.setup(market);
135
+ if (market) {
136
+ await this.node.setup(market);
137
+ }
133
138
  /**
134
139
  * this variable was added to know when the node has gone past the setup/checks stages
135
140
  * and is now starting job work and queueing,
@@ -154,7 +159,7 @@ export default class NodeManager {
154
159
  *
155
160
  * Enter the queue to wait for a job since we have no pending jobs.
156
161
  */
157
- await this.node.queue(market.address.toString());
162
+ await this.node.queue(market ? market.address.toString() : marketArg);
158
163
  }
159
164
  /**
160
165
  *
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * Forward the events the podman container leaves in the directory the two
3
3
  * share. That container has no route to the host manager of its own, so its
4
- * event log is the only sign of whether the CDI watcher ever fired. Each
5
- * event also goes to `onEvent`, for the node to act on.
4
+ * event log is the only sign of whether the CDI watcher ever fired.
6
5
  *
7
6
  * Nothing here may throw: this is diagnostics, and the node's uncaught handler
8
7
  * would turn a failure to read a log file into the node stopping. Watching can
@@ -10,4 +9,4 @@
10
9
  * and a watch already established still reports errors of its own if the
11
10
  * directory it covers is removed.
12
11
  */
13
- export declare function reportPodmanDiagnostics(configLocation: string, onEvent?: (event: string) => void): void;
12
+ export declare function reportPodmanDiagnostics(configLocation: string): void;
@@ -10,8 +10,7 @@ const MAX_CATCH_UP = 1024 * 1024;
10
10
  /**
11
11
  * Forward the events the podman container leaves in the directory the two
12
12
  * share. That container has no route to the host manager of its own, so its
13
- * event log is the only sign of whether the CDI watcher ever fired. Each
14
- * event also goes to `onEvent`, for the node to act on.
13
+ * event log is the only sign of whether the CDI watcher ever fired.
15
14
  *
16
15
  * Nothing here may throw: this is diagnostics, and the node's uncaught handler
17
16
  * would turn a failure to read a log file into the node stopping. Watching can
@@ -19,7 +18,7 @@ const MAX_CATCH_UP = 1024 * 1024;
19
18
  * and a watch already established still reports errors of its own if the
20
19
  * directory it covers is removed.
21
20
  */
22
- export function reportPodmanDiagnostics(configLocation, onEvent) {
21
+ export function reportPodmanDiagnostics(configLocation) {
23
22
  if (!configLocation)
24
23
  return;
25
24
  const log = path.join(configLocation.replace(/^~/, os.homedir()), CDI_EVENTS);
@@ -76,15 +75,12 @@ export function reportPodmanDiagnostics(configLocation, onEvent) {
76
75
  partial = lines.pop() ?? '';
77
76
  lines
78
77
  .filter(Boolean)
79
- .forEach((event) => {
80
- void reportError({
81
- error_type: 'cdiEvent',
82
- error_name: 'Event',
83
- error_message: event,
84
- error_stack: '',
85
- });
86
- onEvent?.(event);
87
- });
78
+ .forEach((event) => void reportError({
79
+ error_type: 'cdiEvent',
80
+ error_name: 'Event',
81
+ error_message: event,
82
+ error_stack: '',
83
+ }));
88
84
  }
89
85
  finally {
90
86
  fs.closeSync(fd);
@@ -15,7 +15,6 @@ export declare class BasicNode {
15
15
  private resourceManager;
16
16
  private provider;
17
17
  private containerOrchestration;
18
- private podmanManager?;
19
18
  private exiting;
20
19
  private sdk;
21
20
  constructor(options: {
@@ -28,8 +27,6 @@ export declare class BasicNode {
28
27
  isApiActive(): Promise<boolean>;
29
28
  node(): string;
30
29
  getSystemEnvironment(): string;
31
- /** An event line the podman container's CDI watcher logged. */
32
- podmanEvent(event: string): void;
33
30
  stop(): Promise<void>;
34
31
  clean(): Promise<void>;
35
32
  start(): Promise<void>;
@@ -14,7 +14,6 @@ import { ExpiryHandler } from './expiry/expiryHandler.js';
14
14
  import { GridHandler } from './grid/gridHandler.js';
15
15
  import { ResourceManager } from './resource/resourceManager.js';
16
16
  import { selectContainerOrchestrationProvider } from '../provider/containerOrchestration/selectContainerOrchestration.js';
17
- import { DockerContainerOrchestration } from '../provider/containerOrchestration/docker/index.js';
18
17
  import { RegisterHandler } from './register/index.js';
19
18
  import { BalanceHandler } from './balance/balanceHandler.js';
20
19
  import { abortControllerSelector, nodeAbortControllerSelector, } from './abort/abortControllerSelector.js';
@@ -22,7 +21,6 @@ import { pollForRun } from './utils/poll.js';
22
21
  import { configs } from '../configs/configs.js';
23
22
  import { TaskManagerRegistry } from './task/TaskManagerRegistry.js';
24
23
  import { StopReasons } from './task/TaskManager.js';
25
- import { isStaleCdiEvent, PodmanManager, } from '../provider/podmanManager/PodmanManager.js';
26
24
  export class BasicNode {
27
25
  constructor(options) {
28
26
  this.exiting = false;
@@ -30,17 +28,8 @@ export class BasicNode {
30
28
  const db = new DB(options.config).db;
31
29
  this.repository = new NodeRepository(db);
32
30
  this.containerOrchestration = selectContainerOrchestrationProvider(options.provider, options.podman, options.gpu, options.trustedExecutionRuntime);
33
- if (options.managedPodman) {
34
- this.podmanManager = new PodmanManager(new DockerContainerOrchestration(options.dockerSocket, options.gpu), options.config, this.containerOrchestration);
35
- }
36
31
  this.resourceManager = new ResourceManager(this.containerOrchestration, this.repository);
37
32
  this.provider = new Provider(this.containerOrchestration, this.repository, this.resourceManager);
38
- if (this.podmanManager) {
39
- this.provider.gpuRuntime = this.podmanManager;
40
- // the API proxy runs in docker beside the node, where replacing podman
41
- // cannot reach it
42
- this.provider.apiOrchestration = this.podmanManager.hostDocker;
43
- }
44
33
  this.apiHandler = new ApiHandler(this.sdk, this.repository, this.provider, options.port);
45
34
  this.balanceHandler = new BalanceHandler(this.sdk);
46
35
  this.gridHandler = new GridHandler(this.sdk, this.repository);
@@ -57,13 +46,6 @@ export class BasicNode {
57
46
  await this.registerHandler.register();
58
47
  }
59
48
  async healthcheck() {
60
- /**
61
- * the podman container, when this node owns it: recreated between jobs,
62
- * so a driver the host replaced since is injected afresh. Nothing of the
63
- * node's runs inside it at this point, and the image and resource caches
64
- * live on volumes that survive it.
65
- */
66
- await this.podmanManager?.recreate('between jobs');
67
49
  /**
68
50
  * run health check,
69
51
  */
@@ -94,11 +76,6 @@ export class BasicNode {
94
76
  getSystemEnvironment() {
95
77
  return this.repository.getNodeInfo().system_environment;
96
78
  }
97
- /** An event line the podman container's CDI watcher logged. */
98
- podmanEvent(event) {
99
- if (isStaleCdiEvent(event))
100
- this.podmanManager?.markStale(event);
101
- }
102
79
  async stop() {
103
80
  await this.marketHandler.stop();
104
81
  await this.runHandler.stop();
@@ -121,11 +98,6 @@ export class BasicNode {
121
98
  if (await this.balanceHandler.balance()) {
122
99
  await this.balanceHandler.check(true);
123
100
  }
124
- /**
125
- * the podman container, when this node owns it: brought up if it is not
126
- * running, and recreated if its GPU mounts went stale since the last job
127
- */
128
- await this.podmanManager?.ensure();
129
101
  /**
130
102
  * get an instance to the container
131
103
  */
@@ -147,10 +119,6 @@ export class BasicNode {
147
119
  try {
148
120
  let resolvedStatus = StopReasons.COMPLETED;
149
121
  const periodicHealthcheck = async () => {
150
- // Leaves the queue, so the healthcheck that follows recreates podman
151
- // before the node takes a job it cannot run.
152
- if (this.podmanManager?.isStale())
153
- return false;
154
122
  const { status, error } = await this.containerOrchestration.healthy();
155
123
  if (!status) {
156
124
  return false;
@@ -6,8 +6,8 @@ import { configs } from '../../configs/configs.js';
6
6
  import { sleep } from '../../utils/utils.js';
7
7
  import { stateStreaming } from '../../monitoring/streaming/StateStreamer.js';
8
8
  import { applyLoggingProxyToClass } from '../../monitoring/proxy/loggingProxy.js';
9
- import { verifyJobOwnerSignatureMiddleware, verifyWSJobOwnerSignatureMiddleware, verifyWSNodeOrJobOwnerSignatureMiddleware, } from './middlewares/index.js';
10
- import { getNodeInfoRoute, getJobResultsRoute, getServiceUrlRoute, postJobDefinitionRoute, postServiceStopRoute, wssLogRoute, wssStatusRoute, getCurrentGroupStatusHandler, getGroupStatusHandler, getOperationsStatusHandler, getOperationStatusHandler, restartGroupOperationHandler, restartOperationHandler, stopGroupOperationHandler, stopOperationHandler, wssTaskManagerLogRoute, getJobDefinitionRoute, getJobInfoRoute, getJobStatsRoute, getJobStatsStreamRoute, } from './routes/index.js';
9
+ import { verifyJobOwnerSignatureMiddleware, verifyWSJobOwnerSignatureMiddleware, } from './middlewares/index.js';
10
+ import { getNodeInfoRoute, getJobResultsRoute, getServiceUrlRoute, postJobDefinitionRoute, postServiceStopRoute, wssLogRoute, wssStatusRoute, wssTerminalRoute, getCurrentGroupStatusHandler, getGroupStatusHandler, getOperationsStatusHandler, getOperationStatusHandler, restartGroupOperationHandler, restartOperationHandler, stopGroupOperationHandler, stopOperationHandler, wssTaskManagerLogRoute, getJobDefinitionRoute, getJobInfoRoute, getJobStatsRoute, getJobStatsStreamRoute, postSshAuthorizeRoute, getSshKeysRoute, deleteSshKeyRoute, } from './routes/index.js';
11
11
  import { requestExit } from '../../../exitCodes.js';
12
12
  import { NodeAlreadyActiveError } from '../../errors/NodeAlreadyActiveError.js';
13
13
  import { reportError } from '../../monitoring/reportError.js';
@@ -91,7 +91,13 @@ export class ApiHandler {
91
91
  });
92
92
  this.wss.on('connection', (ws) => {
93
93
  let keepAliveInterval;
94
+ let terminalActive = false;
94
95
  ws.on('message', async (message) => {
96
+ // Once a terminal session has taken over the socket, the terminal
97
+ // route owns all subsequent frames (stdin/resize), so skip the generic
98
+ // JSON dispatch for them.
99
+ if (terminalActive)
100
+ return;
95
101
  try {
96
102
  const { path, header, body } = JSON.parse(message.toString());
97
103
  keepAliveInterval = setInterval(() => {
@@ -105,7 +111,11 @@ export class ApiHandler {
105
111
  await verifyWSJobOwnerSignatureMiddleware(ws, header, body, wssTaskManagerLogRoute);
106
112
  break;
107
113
  case '/status':
108
- await verifyWSNodeOrJobOwnerSignatureMiddleware(ws, header, body, wssStatusRoute);
114
+ await verifyWSJobOwnerSignatureMiddleware(ws, header, body, wssStatusRoute);
115
+ break;
116
+ case '/terminal':
117
+ terminalActive = true;
118
+ await wssTerminalRoute(ws, this.provider, this.address, body);
109
119
  break;
110
120
  default:
111
121
  ws.close(3003, 'Invalid Path');
@@ -206,6 +216,9 @@ export class ApiHandler {
206
216
  });
207
217
  this.api.use(express.json());
208
218
  this.api.use('/job/:jobId/*', verifyJobOwnerSignatureMiddleware);
219
+ this.api.post('/job/:jobId/ssh/authorize', postSshAuthorizeRoute);
220
+ this.api.get('/job/:jobId/ssh/keys', getSshKeysRoute);
221
+ this.api.delete('/job/:jobId/ssh/keys', deleteSshKeyRoute);
209
222
  // GET Routes
210
223
  this.api.get('/node/info', getNodeInfoRoute);
211
224
  this.api.get('/', (_, res) => res.send(this.address));
@@ -1,4 +1,2 @@
1
1
  export * from './rest/verifyJobOwnerSignatureMiddleware.js';
2
2
  export * from './ws/verifyWSJobOwnerSignatureMiddleware.js';
3
- export * from './ws/verifyWSNodeOrJobOwnerSignatureMiddleware.js';
4
- export * from './ws/verifyWSMiddleware.js';
@@ -2,5 +2,3 @@
2
2
  export * from './rest/verifyJobOwnerSignatureMiddleware.js';
3
3
  // WEBSOCKET MIDDLEWARE
4
4
  export * from './ws/verifyWSJobOwnerSignatureMiddleware.js';
5
- export * from './ws/verifyWSNodeOrJobOwnerSignatureMiddleware.js';
6
- export * from './ws/verifyWSMiddleware.js';
@@ -17,6 +17,7 @@ export async function verifyJobOwnerSignatureMiddleware(req, res, next) {
17
17
  key: 'authorization',
18
18
  expiry: job.timeout,
19
19
  publicKey: new PublicKey(job.project),
20
+ expected_message: jobId,
20
21
  })) {
21
22
  return res.status(401).send('Unauthorized Request');
22
23
  }
@@ -16,6 +16,7 @@ export async function verifyWSJobOwnerSignatureMiddleware(ws, headers, body, nex
16
16
  try {
17
17
  if (sdk.authorization.validate(headers, {
18
18
  publicKey: new PublicKey(job.project),
19
+ expected_message: jobId,
19
20
  })) {
20
21
  await nextFunction(ws, headers, body);
21
22
  }
@@ -11,13 +11,15 @@ export function getNodeInfoRoute(req, res) {
11
11
  ...info,
12
12
  gpus: {
13
13
  ...info.gpus,
14
- devices: info.gpus.devices.map(({ index, name, uuid, memory, network_architecture }) => ({
15
- index,
16
- name,
17
- uuid,
18
- memory,
19
- network_architecture,
20
- })),
14
+ devices: info.gpus.devices
15
+ ? info.gpus.devices.map(({ index, name, uuid, memory, network_architecture }) => ({
16
+ index,
17
+ name,
18
+ uuid,
19
+ memory,
20
+ network_architecture,
21
+ }))
22
+ : [],
21
23
  },
22
24
  network: networkRedacted,
23
25
  },
@@ -1,3 +1,4 @@
1
1
  export * from './job-definition.js';
2
2
  export * from './service-stop.js';
3
3
  export * from './job-operation-actions.js';
4
+ export * from './ssh-authorize.js';
@@ -1,3 +1,4 @@
1
1
  export * from './job-definition.js';
2
2
  export * from './service-stop.js';
3
3
  export * from './job-operation-actions.js';
4
+ export * from './ssh-authorize.js';
@@ -0,0 +1,17 @@
1
+ import { Response } from 'express';
2
+ import { NodeAPIRequest } from '../../types/index.js';
3
+ export type SshAuthorizationRequestBody = {
4
+ sshPublicKey: string;
5
+ expiresAt?: string;
6
+ };
7
+ export declare function postSshAuthorizeRoute(req: NodeAPIRequest<{
8
+ jobId: string;
9
+ }, SshAuthorizationRequestBody>, res: Response): Promise<void>;
10
+ export declare function getSshKeysRoute(req: NodeAPIRequest<{
11
+ jobId: string;
12
+ }>, res: Response): Promise<void>;
13
+ export declare function deleteSshKeyRoute(req: NodeAPIRequest<{
14
+ jobId: string;
15
+ }, {
16
+ sshPublicKey: string;
17
+ }>, res: Response): Promise<void>;
@@ -0,0 +1,53 @@
1
+ import { SSH_SIDECAR_USER } from '../../../../provider/sshSidecar.js';
2
+ import { isActiveSshFlow, SshAccessError, validateSshKey, validateSshExpiry, } from '../../../../provider/SshAccessManager.js';
3
+ /**
4
+ * The job owner's signature has already been checked by the shared
5
+ * verifyJobOwnerSignatureMiddleware; this only maps the SSH operation onto
6
+ * an active flow and onto HTTP statuses.
7
+ */
8
+ async function handleSshRequest(req, res, action) {
9
+ try {
10
+ const jobId = req.params.jobId;
11
+ const flow = req.repository?.getFlow(jobId);
12
+ if (!flow || !isActiveSshFlow(flow))
13
+ throw new SshAccessError('Could not find active job', 404);
14
+ if (!req.provider)
15
+ throw new SshAccessError('SSH provider is not available', 503);
16
+ const result = await action(jobId);
17
+ res
18
+ .status(result.status ?? 200)
19
+ .json({ job: jobId, sshUser: SSH_SIDECAR_USER, ...result.body });
20
+ }
21
+ catch (error) {
22
+ res.status(error instanceof SshAccessError ? error.status : 502).json({
23
+ error: error instanceof SshAccessError
24
+ ? error.message
25
+ : 'Could not apply SSH access on the node',
26
+ });
27
+ }
28
+ }
29
+ export async function postSshAuthorizeRoute(req, res) {
30
+ return handleSshRequest(req, res, async (jobId) => {
31
+ const sshPublicKey = validateSshKey(req.body?.sshPublicKey);
32
+ const expiresAt = validateSshExpiry(req.body?.expiresAt);
33
+ await req.provider.authorizeSshKeyForJob(jobId, sshPublicKey, expiresAt);
34
+ return {
35
+ status: 201,
36
+ body: {
37
+ authorized: true,
38
+ ...(expiresAt === undefined ? {} : { expiresAt }),
39
+ },
40
+ };
41
+ });
42
+ }
43
+ export async function getSshKeysRoute(req, res) {
44
+ return handleSshRequest(req, res, async (jobId) => ({
45
+ body: { keys: await req.provider.listSshKeysForJob(jobId) },
46
+ }));
47
+ }
48
+ export async function deleteSshKeyRoute(req, res) {
49
+ return handleSshRequest(req, res, async (jobId) => {
50
+ await req.provider.revokeSshKeyForJob(jobId, validateSshKey(req.body?.sshPublicKey));
51
+ return { body: { revoked: true } };
52
+ });
53
+ }
@@ -1,2 +1,4 @@
1
1
  export * from './log.js';
2
2
  export * from './status.js';
3
+ export * from './terminal/terminal.js';
4
+ export * from './terminal/terminalAuthorization.js';
@@ -1,2 +1,4 @@
1
1
  export * from './log.js';
2
2
  export * from './status.js';
3
+ export * from './terminal/terminal.js';
4
+ export * from './terminal/terminalAuthorization.js';
@@ -1,4 +1,5 @@
1
1
  import WebSocket from 'ws';
2
+ import { sleep } from '@nosana/sdk';
2
3
  import { logStreaming } from '../../../../monitoring/streaming/LogStreamer.js';
3
4
  import { getSDK } from '../../../../sdk/index.js';
4
5
  import { TaskManagerRegistry } from '../../../task/TaskManagerRegistry.js';
@@ -19,7 +20,7 @@ export async function wssTaskManagerLogRoute(ws, _, { jobAddress, group, opId, t
19
20
  // definition and registering its TaskManager. Keep the authenticated socket
20
21
  // open during that short startup window instead of forcing clients to race it.
21
22
  while (!task && Date.now() < deadline && ws.readyState === WebSocket.OPEN) {
22
- await new Promise((resolve) => setTimeout(resolve, 250));
23
+ await sleep(0.25);
23
24
  task = registry.get(jobAddress);
24
25
  }
25
26
  if (!task)
@@ -0,0 +1,21 @@
1
+ import WebSocket from 'ws';
2
+ import { PublicKey } from '@solana/web3.js';
3
+ import { Provider } from '../../../../../provider/Provider.js';
4
+ import { TerminalAuthorizationRequest } from './terminalAuthorization.js';
5
+ export type TerminalWSBody = TerminalAuthorizationRequest & {
6
+ jobAddress: string;
7
+ cols?: number;
8
+ rows?: number;
9
+ };
10
+ /**
11
+ * Validates a wallet-signed terminal grant and bridges an interactive PTY exec
12
+ * session in the job container to the websocket.
13
+ *
14
+ * Wire protocol after the (already validated) handshake frame:
15
+ * - client -> node : JSON text frames
16
+ * { "type": "stdin", "data": "<string>" }
17
+ * { "type": "resize", "cols": <n>, "rows": <n> }
18
+ * - node -> client : binary frames carry raw stdout/stderr bytes
19
+ * JSON text frame { "type": "exit", "code": <n> } on shell exit
20
+ */
21
+ export declare function wssTerminalRoute(ws: WebSocket, provider: Provider, nodeAddress: PublicKey, body: TerminalWSBody): Promise<void>;