@iicp/client 0.7.11 → 0.7.32
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/README.md +13 -0
- package/dist/cip_policy.d.ts +9 -0
- package/dist/cip_policy.d.ts.map +1 -1
- package/dist/cip_policy.js +13 -0
- package/dist/cip_policy.js.map +1 -1
- package/dist/cli.d.ts +24 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +199 -32
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +124 -36
- package/dist/client.js.map +1 -1
- package/dist/confidentiality.d.ts +14 -0
- package/dist/confidentiality.d.ts.map +1 -0
- package/dist/confidentiality.js +104 -0
- package/dist/confidentiality.js.map +1 -0
- package/dist/instance_lock.d.ts +11 -0
- package/dist/instance_lock.d.ts.map +1 -0
- package/dist/instance_lock.js +86 -0
- package/dist/instance_lock.js.map +1 -0
- package/dist/node.d.ts +30 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +116 -17
- package/dist/node.js.map +1 -1
- package/dist/node_log.d.ts +5 -0
- package/dist/node_log.d.ts.map +1 -0
- package/dist/node_log.js +69 -0
- package/dist/node_log.js.map +1 -0
- package/dist/qualify.js +2 -2
- package/dist/qualify.js.map +1 -1
- package/dist/types.d.ts +12 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,6 +31,19 @@ npm install iicp-client
|
|
|
31
31
|
|
|
32
32
|
---
|
|
33
33
|
|
|
34
|
+
## Architecture — consumer or provider?
|
|
35
|
+
|
|
36
|
+
This SDK covers **both** sides of the IICP protocol:
|
|
37
|
+
|
|
38
|
+
| Role | What you do | Class |
|
|
39
|
+
|------|-------------|-------|
|
|
40
|
+
| **Consumer** | Send AI tasks to the mesh; discover and submit | `IicpClient` |
|
|
41
|
+
| **Provider** | Run a node, register with the directory, serve tasks | `IicpNode` |
|
|
42
|
+
|
|
43
|
+
Consumer and provider can run in the same process. For production provider nodes backed by Ollama/vLLM, see [iicp.network/docs/node-setup](https://iicp.network/docs/node-setup).
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
34
47
|
## Quickstart
|
|
35
48
|
|
|
36
49
|
```typescript
|
package/dist/cip_policy.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface CooperativeInferencePolicyOptions {
|
|
|
16
16
|
/** Bounded to [1, 60000]ms. */
|
|
17
17
|
maxWorkerTimeoutMs?: number;
|
|
18
18
|
maxConcurrentRemote?: number;
|
|
19
|
+
/** #403 — allow tool-execution-domain intents (default false). */
|
|
20
|
+
allowToolExecution?: boolean;
|
|
19
21
|
}
|
|
20
22
|
/** Safe-by-default CIP policy with the S.12 §2.2 capacity gate built-in. */
|
|
21
23
|
export declare class CooperativeInferencePolicy {
|
|
@@ -25,8 +27,15 @@ export declare class CooperativeInferencePolicy {
|
|
|
25
27
|
readonly maxReplicas: number;
|
|
26
28
|
readonly maxWorkerTimeoutMs: number;
|
|
27
29
|
readonly maxConcurrentRemote: number;
|
|
30
|
+
readonly allowToolExecution: boolean;
|
|
28
31
|
private _inFlight;
|
|
29
32
|
constructor(opts?: CooperativeInferencePolicyOptions);
|
|
33
|
+
/**
|
|
34
|
+
* #403 — per-task admission: reject tool-execution-domain intents unless the
|
|
35
|
+
* operator opted in via allowToolExecution. Mirrors the adapter cip_gate.
|
|
36
|
+
* Intent URN form: urn:iicp:intent:<domain>:... — domain is segment index 3.
|
|
37
|
+
*/
|
|
38
|
+
permitsIntent(intent: string): boolean;
|
|
30
39
|
/** CIP-W01: returns true if this node may act as a CIP coordinator. */
|
|
31
40
|
checkCoordinator(): boolean;
|
|
32
41
|
/** CIP-W02: returns true if this node may accept CIP worker tasks. */
|
package/dist/cip_policy.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cip_policy.d.ts","sourceRoot":"","sources":["../src/cip_policy.ts"],"names":[],"mappings":"AACA;;;;;;;;;GASG;AAEH,MAAM,WAAW,iCAAiC;IAChD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,4EAA4E;AAC5E,qBAAa,0BAA0B;IACrC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,OAAO,CAAC,SAAS,CAAK;gBAEV,IAAI,GAAE,iCAAsC;
|
|
1
|
+
{"version":3,"file":"cip_policy.d.ts","sourceRoot":"","sources":["../src/cip_policy.ts"],"names":[],"mappings":"AACA;;;;;;;;;GASG;AAEH,MAAM,WAAW,iCAAiC;IAChD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kEAAkE;IAClE,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,4EAA4E;AAC5E,qBAAa,0BAA0B;IACrC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IACrC,OAAO,CAAC,SAAS,CAAK;gBAEV,IAAI,GAAE,iCAAsC;IAWxD;;;;OAIG;IACH,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAKtC,uEAAuE;IACvE,gBAAgB,IAAI,OAAO;IAI3B,sEAAsE;IACtE,WAAW,IAAI,OAAO;IAItB;;;;;OAKG;IACH,iBAAiB,IAAI,OAAO;IAM5B,uDAAuD;IACvD,cAAc,IAAI,IAAI;IAItB;;;;OAIG;IACH,qBAAqB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAOjD;AAMD,uFAAuF;AACvF,wBAAgB,YAAY,IAAI,0BAA0B,CAEzD;AAED,gFAAgF;AAChF,wBAAgB,kBAAkB,CAChC,IAAI,GAAE,iCAAsC,GAC3C,0BAA0B,CAG5B"}
|
package/dist/cip_policy.js
CHANGED
|
@@ -22,6 +22,7 @@ class CooperativeInferencePolicy {
|
|
|
22
22
|
maxReplicas;
|
|
23
23
|
maxWorkerTimeoutMs;
|
|
24
24
|
maxConcurrentRemote;
|
|
25
|
+
allowToolExecution;
|
|
25
26
|
_inFlight = 0;
|
|
26
27
|
constructor(opts = {}) {
|
|
27
28
|
this.enabled = opts.enabled ?? false;
|
|
@@ -30,6 +31,17 @@ class CooperativeInferencePolicy {
|
|
|
30
31
|
this.maxReplicas = Math.max(1, opts.maxReplicas ?? 3);
|
|
31
32
|
this.maxWorkerTimeoutMs = Math.max(1, Math.min(60_000, opts.maxWorkerTimeoutMs ?? 30_000));
|
|
32
33
|
this.maxConcurrentRemote = Math.max(1, opts.maxConcurrentRemote ?? 2);
|
|
34
|
+
// #403 — per-task admission: tool-execution intents rejected unless opted in.
|
|
35
|
+
this.allowToolExecution = opts.allowToolExecution ?? false;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* #403 — per-task admission: reject tool-execution-domain intents unless the
|
|
39
|
+
* operator opted in via allowToolExecution. Mirrors the adapter cip_gate.
|
|
40
|
+
* Intent URN form: urn:iicp:intent:<domain>:... — domain is segment index 3.
|
|
41
|
+
*/
|
|
42
|
+
permitsIntent(intent) {
|
|
43
|
+
const domain = intent.split(":")[3] ?? "";
|
|
44
|
+
return this.allowToolExecution || domain !== "tool";
|
|
33
45
|
}
|
|
34
46
|
/** CIP-W01: returns true if this node may act as a CIP coordinator. */
|
|
35
47
|
checkCoordinator() {
|
|
@@ -66,6 +78,7 @@ class CooperativeInferencePolicy {
|
|
|
66
78
|
return {};
|
|
67
79
|
return {
|
|
68
80
|
allow_remote_inference: this.allowWorker,
|
|
81
|
+
allow_tool_execution: this.allowToolExecution,
|
|
69
82
|
};
|
|
70
83
|
}
|
|
71
84
|
}
|
package/dist/cip_policy.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cip_policy.js","sourceRoot":"","sources":["../src/cip_policy.ts"],"names":[],"mappings":";AAAA,sCAAsC;AACtC;;;;;;;;;GASG;;;
|
|
1
|
+
{"version":3,"file":"cip_policy.js","sourceRoot":"","sources":["../src/cip_policy.ts"],"names":[],"mappings":";AAAA,sCAAsC;AACtC;;;;;;;;;GASG;;;AA4FH,oCAEC;AAGD,gDAKC;AAxFD,4EAA4E;AAC5E,MAAa,0BAA0B;IAC5B,OAAO,CAAU;IACjB,gBAAgB,CAAU;IAC1B,WAAW,CAAU;IACrB,WAAW,CAAS;IACpB,kBAAkB,CAAS;IAC3B,mBAAmB,CAAS;IAC5B,kBAAkB,CAAU;IAC7B,SAAS,GAAG,CAAC,CAAC;IAEtB,YAAY,OAA0C,EAAE;QACtD,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,KAAK,CAAC;QACrC,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,gBAAgB,IAAI,KAAK,CAAC;QACvD,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,KAAK,CAAC;QAC7C,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,WAAW,IAAI,CAAC,CAAC,CAAC;QACtD,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,kBAAkB,IAAI,MAAM,CAAC,CAAC,CAAC;QAC3F,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,mBAAmB,IAAI,CAAC,CAAC,CAAC;QACtE,8EAA8E;QAC9E,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC,kBAAkB,IAAI,KAAK,CAAC;IAC7D,CAAC;IAED;;;;OAIG;IACH,aAAa,CAAC,MAAc;QAC1B,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC1C,OAAO,IAAI,CAAC,kBAAkB,IAAI,MAAM,KAAK,MAAM,CAAC;IACtD,CAAC;IAED,uEAAuE;IACvE,gBAAgB;QACd,OAAO,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,gBAAgB,CAAC;IAC/C,CAAC;IAED,sEAAsE;IACtE,WAAW;QACT,OAAO,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,WAAW,CAAC;IAC1C,CAAC;IAED;;;;;OAKG;IACH,iBAAiB;QACf,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,mBAAmB;YAAE,OAAO,KAAK,CAAC;QAC7D,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC;QACpB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,uDAAuD;IACvD,cAAc;QACZ,IAAI,IAAI,CAAC,SAAS,GAAG,CAAC;YAAE,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC;IAC9C,CAAC;IAED;;;;OAIG;IACH,qBAAqB;QACnB,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAO,EAAE,CAAC;QAC7B,OAAO;YACL,sBAAsB,EAAE,IAAI,CAAC,WAAW;YACxC,oBAAoB,EAAE,IAAI,CAAC,kBAAkB;SAC9C,CAAC;IACJ,CAAC;CACF;AAtED,gEAsEC;AAED,4EAA4E;AAE5E,IAAI,OAAO,GAA+B,IAAI,0BAA0B,EAAE,CAAC;AAE3E,uFAAuF;AACvF,SAAgB,YAAY;IAC1B,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,gFAAgF;AAChF,SAAgB,kBAAkB,CAChC,OAA0C,EAAE;IAE5C,OAAO,GAAG,IAAI,0BAA0B,CAAC,IAAI,CAAC,CAAC;IAC/C,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { type NodeIdentity } from "./identity.js";
|
|
3
|
+
export interface ServeOpts {
|
|
4
|
+
backendUrl: string;
|
|
5
|
+
backendType: string;
|
|
6
|
+
/** #5 — Bearer key for an auth-requiring OpenAI-compat backend (LM Studio, hosted). Empty = none. */
|
|
7
|
+
backendApiKey: string;
|
|
8
|
+
model: string;
|
|
9
|
+
publicEndpoint: string;
|
|
10
|
+
directoryUrl: string;
|
|
11
|
+
region: string;
|
|
12
|
+
intent: string;
|
|
13
|
+
maxConcurrent: number;
|
|
14
|
+
nodeId: string;
|
|
15
|
+
port: number;
|
|
16
|
+
host: string;
|
|
17
|
+
skipRegistration: boolean;
|
|
18
|
+
force: boolean;
|
|
19
|
+
autoDetectNat: boolean;
|
|
20
|
+
externalIpProbeUrl: string;
|
|
21
|
+
relayWorkerEndpoint: string;
|
|
22
|
+
node: string;
|
|
23
|
+
logDir?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function applySavedNode(opts: ServeOpts, saved: NodeIdentity): ServeOpts;
|
|
2
26
|
export declare function main(argv?: string[]): Promise<number>;
|
|
3
27
|
//# sourceMappingURL=cli.d.ts.map
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAiCA,OAAO,EASL,KAAK,YAAY,EAClB,MAAM,eAAe,CAAC;AAEvB,MAAM,WAAW,SAAS;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,qGAAqG;IACrG,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,OAAO,CAAC;IAC1B,KAAK,EAAE,OAAO,CAAC;IACf,aAAa,EAAE,OAAO,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AA8UD,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,GAAG,SAAS,CAiB9E;AAwaD,wBAAsB,IAAI,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CA8FlF"}
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.applySavedNode = applySavedNode;
|
|
4
5
|
exports.main = main;
|
|
5
6
|
// SPDX-License-Identifier: Apache-2.0
|
|
6
7
|
/**
|
|
@@ -21,12 +22,17 @@ exports.main = main;
|
|
|
21
22
|
*/
|
|
22
23
|
const node_util_1 = require("node:util");
|
|
23
24
|
const node_crypto_1 = require("node:crypto");
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
26
|
+
const SDK_VERSION = require("../package.json").version;
|
|
24
27
|
const net = require("node:net");
|
|
25
28
|
const node_child_process_1 = require("node:child_process");
|
|
26
29
|
const readline = require("node:readline/promises");
|
|
27
30
|
const node_process_1 = require("node:process");
|
|
28
31
|
const node_js_1 = require("./node.js");
|
|
32
|
+
const client_js_1 = require("./client.js");
|
|
33
|
+
const node_log_js_1 = require("./node_log.js");
|
|
29
34
|
const cip_policy_js_1 = require("./cip_policy.js");
|
|
35
|
+
const instance_lock_js_1 = require("./instance_lock.js");
|
|
30
36
|
const index_js_1 = require("./backends/index.js");
|
|
31
37
|
const identity_js_1 = require("./identity.js");
|
|
32
38
|
function envOr(name, fallback) {
|
|
@@ -51,7 +57,8 @@ function printHelp() {
|
|
|
51
57
|
`Commands:\n` +
|
|
52
58
|
` init Interactive wizard — set up operator + first node config\n` +
|
|
53
59
|
` list List node configs saved under ~/.iicp/nodes/\n` +
|
|
54
|
-
` serve Register and serve a node\n
|
|
60
|
+
` serve Register and serve a node\n` +
|
|
61
|
+
` query <prompt> Discover mesh nodes and submit a chat task\n\n` +
|
|
55
62
|
`Run an IICP provider node backed by an OpenAI-compatible server.\n\n` +
|
|
56
63
|
`serve required (flag or env):\n` +
|
|
57
64
|
` --model NAME IICP_BACKEND_MODEL — model name (e.g. qwen2.5:0.5b)\n` +
|
|
@@ -59,6 +66,7 @@ function printHelp() {
|
|
|
59
66
|
`serve optional:\n` +
|
|
60
67
|
` --backend-url URL IICP_BACKEND_URL — Ollama / vLLM / LM Studio (default http://localhost:11434)\n` +
|
|
61
68
|
` --backend-type TYPE IICP_BACKEND_TYPE — openai_compat | vllm | llamacpp (default openai_compat)\n` +
|
|
69
|
+
` --backend-api-key KEY IICP_BACKEND_API_KEY — Bearer key for an auth'd backend (LM Studio, hosted)\n` +
|
|
62
70
|
` --public-endpoint URL IICP_PUBLIC_ENDPOINT — externally reachable URL of this node\n` +
|
|
63
71
|
` --directory-url URL IICP_DIRECTORY_URL (default https://iicp.network/api)\n` +
|
|
64
72
|
` --region REGION IICP_REGION (default eu-central)\n` +
|
|
@@ -66,10 +74,16 @@ function printHelp() {
|
|
|
66
74
|
` --max-concurrent N IICP_MAX_CONCURRENT (default 4)\n` +
|
|
67
75
|
` --node-id ID IICP_NODE_ID (auto-generated if absent)\n` +
|
|
68
76
|
` --port N IICP_PORT (default 9484)\n` +
|
|
69
|
-
` --host HOST IICP_HOST (default
|
|
77
|
+
` --host HOST IICP_HOST (default :: — dual-stack IPv4+IPv6)\n` +
|
|
70
78
|
` --skip-registration IICP_SKIP_REGISTRATION — register-free dev mode\n` +
|
|
71
79
|
` --auto-detect-nat IICP_AUTO_DETECT_NAT — run NAT detection at startup\n` +
|
|
72
|
-
` --external-ip-probe-url U IICP_EXTERNAL_IP_PROBE_URL — fallback IPv4 probe\n`
|
|
80
|
+
` --external-ip-probe-url U IICP_EXTERNAL_IP_PROBE_URL — fallback IPv4 probe\n\n` +
|
|
81
|
+
`query optional:\n` +
|
|
82
|
+
` --directory-url URL IICP_DIRECTORY_URL (default https://iicp.network/api)\n` +
|
|
83
|
+
` --intent URN IICP_INTENT (default urn:iicp:intent:llm:chat:v1)\n` +
|
|
84
|
+
` --model NAME Pin to a specific model on the remote node\n` +
|
|
85
|
+
` --max-tokens N Limit response length\n` +
|
|
86
|
+
` --timeout-ms N Request timeout (default 60000)\n`);
|
|
73
87
|
}
|
|
74
88
|
async function checkDependencies(backendUrl) {
|
|
75
89
|
const out = [];
|
|
@@ -101,7 +115,7 @@ async function checkDependencies(backendUrl) {
|
|
|
101
115
|
out.push({ name: mod, severity: "ok", message: purpose, installable: false, npmExtra: "" });
|
|
102
116
|
}
|
|
103
117
|
catch {
|
|
104
|
-
out.push({ name: mod, severity: "
|
|
118
|
+
out.push({ name: mod, severity: "optional", message: `${purpose} (optional — not installed)`, installable: true, npmExtra: npmName });
|
|
105
119
|
}
|
|
106
120
|
}
|
|
107
121
|
// 3) IPv6 routing surface (advisory)
|
|
@@ -124,14 +138,14 @@ async function checkDependencies(backendUrl) {
|
|
|
124
138
|
return out;
|
|
125
139
|
}
|
|
126
140
|
function printDepStatus(issues) {
|
|
127
|
-
const glyph = { ok: " ✓", warn: " !", missing: " ✗" };
|
|
141
|
+
const glyph = { ok: " ✓", optional: " ○", warn: " !", missing: " ✗" };
|
|
128
142
|
for (const i of issues) {
|
|
129
143
|
process.stdout.write(`${glyph[i.severity] ?? " ?"} ${i.name.padEnd(18)} ${i.message}\n`);
|
|
130
144
|
}
|
|
131
145
|
}
|
|
132
146
|
function installMissing(issues) {
|
|
133
147
|
const extras = Array.from(new Set(issues
|
|
134
|
-
.filter((i) => i.severity === "missing" && i.installable && i.npmExtra)
|
|
148
|
+
.filter((i) => (i.severity === "optional" || i.severity === "missing") && i.installable && i.npmExtra)
|
|
135
149
|
.map((i) => i.npmExtra))).sort();
|
|
136
150
|
if (extras.length === 0)
|
|
137
151
|
return;
|
|
@@ -212,14 +226,14 @@ async function runInit() {
|
|
|
212
226
|
process.stdout.write(`Checking dependencies …\n`);
|
|
213
227
|
const issues = await checkDependencies(backend);
|
|
214
228
|
printDepStatus(issues);
|
|
215
|
-
const
|
|
216
|
-
if (
|
|
217
|
-
const yn = (await ask(rl, `\
|
|
229
|
+
const optionalCount = issues.filter((i) => (i.severity === "optional" || i.severity === "missing") && i.installable).length;
|
|
230
|
+
if (optionalCount > 0) {
|
|
231
|
+
const yn = (await ask(rl, `\nEnable ${optionalCount} optional package(s)? (your node runs without them) [Y/n]`, "y")).toLowerCase();
|
|
218
232
|
if (yn === "" || yn === "y" || yn === "yes") {
|
|
219
233
|
installMissing(issues);
|
|
220
234
|
}
|
|
221
235
|
else {
|
|
222
|
-
process.stdout.write(`
|
|
236
|
+
process.stdout.write(` ○ skipping — enable later with: npm install <pkg>\n`);
|
|
223
237
|
}
|
|
224
238
|
}
|
|
225
239
|
process.stdout.write(`\nDocumentation:\n`);
|
|
@@ -384,6 +398,20 @@ async function runServe(opts) {
|
|
|
384
398
|
return 2;
|
|
385
399
|
}
|
|
386
400
|
const nodeId = (opts.nodeId || (0, node_crypto_1.randomUUID)()).slice(0, 36);
|
|
401
|
+
const logDir = opts.logDir;
|
|
402
|
+
// #405 — single-instance lock: refuse a second LIVE process for this node_id
|
|
403
|
+
// (the token-rotation war). Distinct node_ids are unaffected. Fails open.
|
|
404
|
+
let instanceLock;
|
|
405
|
+
try {
|
|
406
|
+
instanceLock = instance_lock_js_1.InstanceLock.acquire(nodeId, opts.force);
|
|
407
|
+
}
|
|
408
|
+
catch (exc) {
|
|
409
|
+
if (exc instanceof instance_lock_js_1.NodeAlreadyServingError) {
|
|
410
|
+
console.error(`[iicp-node] ${exc.message}`);
|
|
411
|
+
process.exit(2);
|
|
412
|
+
}
|
|
413
|
+
throw exc;
|
|
414
|
+
}
|
|
387
415
|
// Resolve the actual listen port before NAT detection: start at the
|
|
388
416
|
// requested port (default 9484, the official IICP port) and auto-increment
|
|
389
417
|
// to the next free port. Keeps one port per node (multiple models share it)
|
|
@@ -513,44 +541,105 @@ async function runServe(opts) {
|
|
|
513
541
|
const handler = (0, index_js_1.getBackendHandler)(opts.backendType, {
|
|
514
542
|
baseUrl: _baseUrl,
|
|
515
543
|
model: opts.model,
|
|
544
|
+
// #5 — Bearer key for auth'd backends (LM Studio, hosted). Empty/undefined = no header.
|
|
545
|
+
apiKey: opts.backendApiKey || undefined,
|
|
516
546
|
});
|
|
517
547
|
// GAP-6: probe backend for all available models so the registration advertises
|
|
518
548
|
// the full list — not just the single configured model. Best-effort; fall back
|
|
519
549
|
// to the single configured model on any error.
|
|
520
550
|
try {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
551
|
+
// #409 — strip a trailing /v1 to a root so probe URLs are well-formed for
|
|
552
|
+
// both Ollama (`http://host:11434`) and LM Studio/OpenAI-compat
|
|
553
|
+
// (`http://host:1234/v1`); attach the Bearer key (LM Studio /v1/models 401s
|
|
554
|
+
// without it). Without this, /v1 backends got `…/v1/v1/models` (404) and no
|
|
555
|
+
// models were discovered, so multi-intent never fired.
|
|
556
|
+
const base = opts.backendUrl.replace(/\/$/, "");
|
|
557
|
+
const root = base.endsWith("/v1") ? base.slice(0, -3) : base;
|
|
558
|
+
const headers = opts.backendApiKey
|
|
559
|
+
? { Authorization: `Bearer ${opts.backendApiKey}` }
|
|
560
|
+
: {};
|
|
561
|
+
const allModels = await (async () => {
|
|
562
|
+
// Ollama /api/tags ({models:[{name}]})
|
|
563
|
+
try {
|
|
564
|
+
const r = await fetch(`${root}/api/tags`, { headers, signal: AbortSignal.timeout(3000) });
|
|
565
|
+
if (r.ok) {
|
|
566
|
+
const d = await r.json();
|
|
567
|
+
const names = (d.models ?? []).map((m) => m.name);
|
|
568
|
+
if (names.length > 0)
|
|
569
|
+
return names;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
catch { /* try OpenAI next */ }
|
|
573
|
+
// OpenAI-compat /v1/models ({data:[{id}]})
|
|
574
|
+
try {
|
|
575
|
+
const r = await fetch(`${root}/v1/models`, { headers, signal: AbortSignal.timeout(3000) });
|
|
576
|
+
if (r.ok) {
|
|
577
|
+
const d = await r.json();
|
|
578
|
+
return (d.data ?? []).map((m) => m.id).filter(Boolean);
|
|
579
|
+
}
|
|
532
580
|
}
|
|
581
|
+
catch { /* best-effort */ }
|
|
582
|
+
return [];
|
|
583
|
+
})();
|
|
584
|
+
const extra = allModels.filter((m) => m !== opts.model);
|
|
585
|
+
if (extra.length > 0) {
|
|
586
|
+
node["_cfg"].capabilities = extra;
|
|
587
|
+
// eslint-disable-next-line no-console
|
|
588
|
+
console.log(`[iicp-node] GAP-6: advertising ${extra.length} additional model(s): ${extra.slice(0, 6).join(", ")}`);
|
|
533
589
|
}
|
|
534
590
|
}
|
|
535
591
|
catch {
|
|
536
592
|
// best-effort; no-op on error
|
|
537
593
|
}
|
|
594
|
+
// NAT-4 guard: if endpoint is non-routable and no relay configured, skip
|
|
595
|
+
// registration to avoid a confusing 422 from the directory's RoutableEndpoint check.
|
|
596
|
+
const epLocal = publicEndpoint.startsWith("http://localhost") ||
|
|
597
|
+
publicEndpoint.startsWith("http://127.") ||
|
|
598
|
+
publicEndpoint.startsWith("http://0.0.0.0") ||
|
|
599
|
+
publicEndpoint.startsWith("http://192.168.") ||
|
|
600
|
+
publicEndpoint.startsWith("http://10.");
|
|
601
|
+
if (epLocal && !opts.relayWorkerEndpoint && !opts.skipRegistration) {
|
|
602
|
+
// eslint-disable-next-line no-console
|
|
603
|
+
console.warn("[iicp-node] no routable endpoint detected and no relay configured — " +
|
|
604
|
+
"skipping directory registration. Node will accept direct connections " +
|
|
605
|
+
"but will not appear in discover results. " +
|
|
606
|
+
"Set IICP_PUBLIC_ENDPOINT=<url> or IICP_RELAY_WORKER_ENDPOINT=<host>:<port> to register.");
|
|
607
|
+
opts.skipRegistration = true;
|
|
608
|
+
}
|
|
609
|
+
// #404 — register with bounded backoff retry. On persistent failure, pass an
|
|
610
|
+
// empty token (NOT undefined) so the heartbeat loop still starts and re-registers
|
|
611
|
+
// on the first 401 (#399 path) once the directory is reachable — the self-healing
|
|
612
|
+
// watchdog. undefined is reserved for --skip-registration (no heartbeat by design).
|
|
538
613
|
let token;
|
|
539
614
|
if (!opts.skipRegistration) {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
615
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
616
|
+
try {
|
|
617
|
+
token = await node.register();
|
|
618
|
+
// eslint-disable-next-line no-console
|
|
619
|
+
console.log(`[iicp-node] registered as ${nodeId} (token=${(token ?? "").slice(0, 8)}…)`);
|
|
620
|
+
(0, node_log_js_1.writeNodeEvent)(nodeId, "register_ok", `endpoint=${opts.publicEndpoint || `http://localhost:${opts.port}`}`, logDir);
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
catch (exc) {
|
|
624
|
+
const msg = exc instanceof Error ? exc.message : String(exc);
|
|
625
|
+
if (attempt >= 3) {
|
|
626
|
+
// eslint-disable-next-line no-console
|
|
627
|
+
console.warn(`[iicp-node] registration failed after ${attempt} attempts: ${msg} — starting heartbeat loop anyway; it will re-register on the first 401`);
|
|
628
|
+
(0, node_log_js_1.writeNodeEvent)(nodeId, "register_fail", `error=${msg} attempts=${attempt}`, logDir);
|
|
629
|
+
token = ""; // empty (not undefined) → heartbeat loop starts and self-heals
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
const backoff = 2 ** attempt;
|
|
633
|
+
// eslint-disable-next-line no-console
|
|
634
|
+
console.warn(`[iicp-node] registration attempt ${attempt} failed: ${msg} — retrying in ${backoff}s`);
|
|
635
|
+
await new Promise((r) => setTimeout(r, backoff * 1000));
|
|
636
|
+
}
|
|
549
637
|
}
|
|
550
638
|
}
|
|
551
639
|
// eslint-disable-next-line no-console
|
|
552
640
|
console.log(`[iicp-node] serving ${opts.intent} on ${opts.host}:${opts.port} — ` +
|
|
553
641
|
`backend ${opts.backendUrl} (model=${opts.model}, max_concurrent=${opts.maxConcurrent})`);
|
|
642
|
+
(0, node_log_js_1.writeNodeEvent)(nodeId, "serve_start", `port=${opts.port} model=${opts.model} intent=${opts.intent}`, logDir);
|
|
554
643
|
// serve() returns a stop() handle but never resolves on its own; we wait for
|
|
555
644
|
// SIGINT/SIGTERM to terminate.
|
|
556
645
|
const stop = node.serve(handler, { host: opts.host, port: opts.port, nodeToken: token });
|
|
@@ -568,13 +657,17 @@ async function runServe(opts) {
|
|
|
568
657
|
try {
|
|
569
658
|
if (token) {
|
|
570
659
|
await node.deregister(token);
|
|
660
|
+
(0, node_log_js_1.writeNodeEvent)(nodeId, "deregister_ok", "", logDir);
|
|
571
661
|
}
|
|
572
662
|
}
|
|
573
663
|
catch (exc) {
|
|
664
|
+
const deregMsg = exc instanceof Error ? exc.message : String(exc);
|
|
574
665
|
// eslint-disable-next-line no-console
|
|
575
|
-
console.warn(`[iicp-node] deregister failed: ${
|
|
666
|
+
console.warn(`[iicp-node] deregister failed: ${deregMsg}`);
|
|
667
|
+
(0, node_log_js_1.writeNodeEvent)(nodeId, "deregister_fail", `error=${deregMsg}`, logDir);
|
|
576
668
|
}
|
|
577
669
|
stop();
|
|
670
|
+
instanceLock.release(); // #405 — free the pidfile on shutdown
|
|
578
671
|
resolve();
|
|
579
672
|
};
|
|
580
673
|
process.once("SIGINT", () => void shutdown("SIGINT"));
|
|
@@ -584,16 +677,84 @@ async function runServe(opts) {
|
|
|
584
677
|
void node_crypto_1.randomUUID;
|
|
585
678
|
return 0;
|
|
586
679
|
}
|
|
680
|
+
async function runQuery(argv) {
|
|
681
|
+
const { values, positionals } = (0, node_util_1.parseArgs)({
|
|
682
|
+
args: argv,
|
|
683
|
+
options: {
|
|
684
|
+
"directory-url": { type: "string" },
|
|
685
|
+
intent: { type: "string" },
|
|
686
|
+
model: { type: "string" },
|
|
687
|
+
"max-tokens": { type: "string" },
|
|
688
|
+
"timeout-ms": { type: "string" },
|
|
689
|
+
},
|
|
690
|
+
allowPositionals: true,
|
|
691
|
+
strict: false,
|
|
692
|
+
});
|
|
693
|
+
const prompt = positionals.join(" ").trim();
|
|
694
|
+
if (!prompt) {
|
|
695
|
+
process.stderr.write("Usage: iicp-node query <prompt> [flags]\n");
|
|
696
|
+
return 1;
|
|
697
|
+
}
|
|
698
|
+
const directoryUrl = values["directory-url"] ??
|
|
699
|
+
process.env["IICP_DIRECTORY_URL"] ??
|
|
700
|
+
"https://iicp.network/api";
|
|
701
|
+
const intent = values["intent"] ??
|
|
702
|
+
process.env["IICP_INTENT"] ??
|
|
703
|
+
"urn:iicp:intent:llm:chat:v1";
|
|
704
|
+
const timeoutMs = parseInt(values["timeout-ms"] ?? "60000", 10);
|
|
705
|
+
const payload = {
|
|
706
|
+
messages: [{ role: "user", content: prompt }],
|
|
707
|
+
};
|
|
708
|
+
if (values["model"])
|
|
709
|
+
payload["model"] = values["model"];
|
|
710
|
+
if (values["max-tokens"])
|
|
711
|
+
payload["max_tokens"] = parseInt(values["max-tokens"], 10);
|
|
712
|
+
const client = new client_js_1.IicpClient({ directory_url: directoryUrl, timeout_ms: timeoutMs, tls_verify: true });
|
|
713
|
+
process.stderr.write(`[iicp-node] Discovering nodes for ${intent}...\n`);
|
|
714
|
+
try {
|
|
715
|
+
const resp = await client.submit({
|
|
716
|
+
task_id: (0, node_crypto_1.randomUUID)(),
|
|
717
|
+
intent,
|
|
718
|
+
payload,
|
|
719
|
+
});
|
|
720
|
+
if (resp.status === "completed" && resp.result) {
|
|
721
|
+
const res = resp.result;
|
|
722
|
+
const content = typeof res["content"] === "string"
|
|
723
|
+
? res["content"]
|
|
724
|
+
: JSON.stringify(resp.result, null, 2);
|
|
725
|
+
process.stdout.write(content + "\n");
|
|
726
|
+
if (resp.metrics?.node_id) {
|
|
727
|
+
process.stderr.write(`[iicp-node] routed to node ${resp.metrics.node_id.slice(0, 8)}\n`);
|
|
728
|
+
}
|
|
729
|
+
if (resp.metrics?.latency_ms != null) {
|
|
730
|
+
process.stderr.write(`[iicp-node] latency ${resp.metrics.latency_ms.toFixed(0)}ms\n`);
|
|
731
|
+
}
|
|
732
|
+
return 0;
|
|
733
|
+
}
|
|
734
|
+
process.stderr.write(`[iicp-node] task status: ${resp.status}\n`);
|
|
735
|
+
return 1;
|
|
736
|
+
}
|
|
737
|
+
catch (e) {
|
|
738
|
+
process.stderr.write(`ERROR: ${e}\n`);
|
|
739
|
+
return 1;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
587
742
|
async function main(argv = process.argv.slice(2)) {
|
|
588
743
|
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
589
744
|
printHelp();
|
|
590
745
|
return argv.length === 0 ? 2 : 0;
|
|
591
746
|
}
|
|
747
|
+
if (argv[0] === "--version" || argv[0] === "-V") {
|
|
748
|
+
process.stdout.write(`iicp-node ${SDK_VERSION}\n`);
|
|
749
|
+
return 0;
|
|
750
|
+
}
|
|
592
751
|
const cmd = argv[0];
|
|
593
752
|
if (cmd === "init")
|
|
594
753
|
return runInit();
|
|
595
754
|
if (cmd === "list")
|
|
596
755
|
return runList();
|
|
756
|
+
if (cmd === "query")
|
|
757
|
+
return runQuery(argv.slice(1));
|
|
597
758
|
if (cmd !== "serve") {
|
|
598
759
|
process.stderr.write(`unknown command: ${cmd}\n`);
|
|
599
760
|
printHelp();
|
|
@@ -605,6 +766,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
605
766
|
node: { type: "string" },
|
|
606
767
|
"backend-url": { type: "string" },
|
|
607
768
|
"backend-type": { type: "string" },
|
|
769
|
+
"backend-api-key": { type: "string" },
|
|
608
770
|
model: { type: "string" },
|
|
609
771
|
"public-endpoint": { type: "string" },
|
|
610
772
|
"directory-url": { type: "string" },
|
|
@@ -615,9 +777,11 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
615
777
|
port: { type: "string" },
|
|
616
778
|
host: { type: "string" },
|
|
617
779
|
"skip-registration": { type: "boolean" },
|
|
780
|
+
force: { type: "boolean" },
|
|
618
781
|
"auto-detect-nat": { type: "boolean" },
|
|
619
782
|
"external-ip-probe-url": { type: "string" },
|
|
620
783
|
"relay-worker-endpoint": { type: "string" },
|
|
784
|
+
"log-dir": { type: "string" },
|
|
621
785
|
help: { type: "boolean", short: "h" },
|
|
622
786
|
},
|
|
623
787
|
allowPositionals: false,
|
|
@@ -631,6 +795,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
631
795
|
backendUrl: values["backend-url"] ?? envOr("IICP_BACKEND_URL") ?? "",
|
|
632
796
|
backendType: values["backend-type"] ??
|
|
633
797
|
envOr("IICP_BACKEND_TYPE", "openai_compat"),
|
|
798
|
+
backendApiKey: values["backend-api-key"] ?? envOr("IICP_BACKEND_API_KEY") ?? "",
|
|
634
799
|
model: values.model ?? envOr("IICP_BACKEND_MODEL") ?? "",
|
|
635
800
|
publicEndpoint: values["public-endpoint"] ?? envOr("IICP_PUBLIC_ENDPOINT") ?? "",
|
|
636
801
|
directoryUrl: values["directory-url"] ??
|
|
@@ -644,8 +809,9 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
644
809
|
port: values.port !== undefined
|
|
645
810
|
? parseInt(values.port, 10)
|
|
646
811
|
: envInt("IICP_PORT", 9484),
|
|
647
|
-
host: values.host ?? envOr("IICP_HOST", "
|
|
812
|
+
host: values.host ?? envOr("IICP_HOST", "::"),
|
|
648
813
|
skipRegistration: Boolean(values["skip-registration"]) || envBool("IICP_SKIP_REGISTRATION"),
|
|
814
|
+
force: Boolean(values["force"]) || envBool("IICP_FORCE"),
|
|
649
815
|
// Default ON — matches Python CLI behaviour; operator must set IICP_AUTO_DETECT_NAT=false to opt out.
|
|
650
816
|
autoDetectNat: values["auto-detect-nat"] !== undefined
|
|
651
817
|
? Boolean(values["auto-detect-nat"])
|
|
@@ -655,6 +821,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
655
821
|
?? envOr("IICP_EXTERNAL_IP_PROBE_URL")
|
|
656
822
|
?? "https://api.ipify.org",
|
|
657
823
|
relayWorkerEndpoint: values["relay-worker-endpoint"] ?? envOr("IICP_RELAY_WORKER_ENDPOINT") ?? "",
|
|
824
|
+
logDir: values["log-dir"] ?? envOr("IICP_LOG_DIR"),
|
|
658
825
|
};
|
|
659
826
|
return runServe(opts);
|
|
660
827
|
}
|