cicy-desktop 2.1.327 → 2.1.328
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/package.json +5 -5
- package/src/backends/hub-client.js +6 -0
- package/src/utils/hub-trust.js +106 -34
- package/test/rpc-guard-owner-hub.test.js +17 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cicy-desktop",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.328",
|
|
4
4
|
"description": "CiCy - AI-powered operating system browser",
|
|
5
5
|
"main": "src/main.js",
|
|
6
6
|
"bin": {
|
|
@@ -144,10 +144,10 @@
|
|
|
144
144
|
"//optionalDependencies": "(2026-06 回调): mac/linux 改回 native cicy-code(:8008,colima 在 16G mac 上压垮内存)→ 重新内置 cicy-code-<plat> / cicy-mihomo-<plat>,localbin.fromBundle 零网络 seed(npm 仅作更新通道,带 npmmirror→npmjs 回退)。这些由 scripts/sync-runtime-deps.cjs 在 tag-push 时同步到最新版。npm 的 os/cpu 字段保证每个平台只装自己那份。Windows 仍走 docker(WSL :8009),它那份 cicy-code-windows 用不到但 bundle 着无害。",
|
|
145
145
|
"optionalDependencies": {
|
|
146
146
|
"electron": "41.0.3",
|
|
147
|
-
"cicy-code-darwin-x64": "2.3.
|
|
148
|
-
"cicy-code-darwin-arm64": "2.3.
|
|
149
|
-
"cicy-code-linux-x64": "2.3.
|
|
150
|
-
"cicy-code-linux-arm64": "2.3.
|
|
147
|
+
"cicy-code-darwin-x64": "2.3.596",
|
|
148
|
+
"cicy-code-darwin-arm64": "2.3.596",
|
|
149
|
+
"cicy-code-linux-x64": "2.3.596",
|
|
150
|
+
"cicy-code-linux-arm64": "2.3.596",
|
|
151
151
|
"cicy-code-windows-x64": "2.3.193",
|
|
152
152
|
"cicy-mihomo-darwin-x64": "1.10.4",
|
|
153
153
|
"cicy-mihomo-darwin-arm64": "1.10.4",
|
|
@@ -352,6 +352,12 @@ async function instances() {
|
|
|
352
352
|
agents: Array.isArray(i.agents) ? i.agents.length : undefined,
|
|
353
353
|
}))
|
|
354
354
|
.sort((x, y) => Number(y.online) - Number(x.online) || x.name.localeCompare(y.name));
|
|
355
|
+
// Every host in this list was fetched with the owner's own token, so each one
|
|
356
|
+
// provably belongs to the owner's tenant — trust the whole set for owner-scoped
|
|
357
|
+
// dangerous RPC (each node is served at its own <node>.hub.cicy-ai.com subdomain).
|
|
358
|
+
try {
|
|
359
|
+
hubTrust.recordOwnerHubHosts(out.map((i) => i.host));
|
|
360
|
+
} catch {}
|
|
355
361
|
return { ok: true, owner: res.json.owner || a.owner, viaSidecar, instances: out };
|
|
356
362
|
}
|
|
357
363
|
|
package/src/utils/hub-trust.js
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
// Copyright 2026 CiCy AI
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
// Owner-hub trust — the
|
|
5
|
-
// commanding it", so rpc_call from the operator's own ws-hub control surface
|
|
6
|
-
// drive dangerous tools (exec_*/file_*/
|
|
7
|
-
// per-call consent modal
|
|
4
|
+
// Owner-hub trust — the SET of hub hosts a hub-logged-in desktop treats as "its
|
|
5
|
+
// owner commanding it", so rpc_call from the operator's own ws-hub control surface
|
|
6
|
+
// (or any of the owner's own nodes) can drive dangerous tools (exec_*/file_*/
|
|
7
|
+
// electron_inject) on this machine WITHOUT a per-call consent modal nobody is
|
|
8
|
+
// present to click.
|
|
8
9
|
//
|
|
9
|
-
// Scope (
|
|
10
|
-
// •
|
|
11
|
-
// https://<
|
|
12
|
-
//
|
|
13
|
-
// •
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
10
|
+
// Scope (the narrowest boundary that still gives fleet control):
|
|
11
|
+
// • Each of the owner's nodes is served at its OWN subdomain
|
|
12
|
+
// https://<node>.hub.cicy-ai.com (confirmed from the live instances list) —
|
|
13
|
+
// so owner-trust is a SET of hosts, not one.
|
|
14
|
+
// • Every host in the set is captured ONLY from an authenticated source proven
|
|
15
|
+
// to belong to THIS desktop's owner email:
|
|
16
|
+
// – a hub grant minted with this desktop's own hubAuth token (grantUrl), and
|
|
17
|
+
// – the instances() list fetched with that same token.
|
|
18
|
+
// Both prove the host is the owner's; no origin-string guessing, no
|
|
19
|
+
// email→slug derivation, no bare `.hub.cicy-ai.com` wildcard (that would
|
|
20
|
+
// trust a DIFFERENT tenant's subdomain).
|
|
21
|
+
// • Persisted under hubAuth so it survives restart; cleared on logout.
|
|
22
|
+
// • Every host is suffix-locked to an exact single-label <tenant>.hub.cicy-ai.com.
|
|
21
23
|
const os = require("os");
|
|
22
24
|
const path = require("path");
|
|
23
25
|
const { readGlobalConfig, updateGlobalConfig } = require("./global-json");
|
|
@@ -62,55 +64,125 @@ function loggedIn() {
|
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
// The trusted host set, read fresh from disk. Honours a legacy single
|
|
68
|
+
// `ownerHubHost` string alongside the `ownerHubHosts` array.
|
|
69
|
+
function _readHosts() {
|
|
66
70
|
try {
|
|
67
|
-
|
|
71
|
+
const a = readGlobalConfig(GLOBAL_JSON)?.hubAuth;
|
|
72
|
+
if (!a) return [];
|
|
73
|
+
const out = [];
|
|
74
|
+
const push = (h) => {
|
|
75
|
+
const n = normHubHost(h);
|
|
76
|
+
if (n && !out.includes(n)) out.push(n);
|
|
77
|
+
};
|
|
78
|
+
if (Array.isArray(a.ownerHubHosts)) a.ownerHubHosts.forEach(push);
|
|
79
|
+
if (a.ownerHubHome) push(a.ownerHubHome);
|
|
80
|
+
if (a.ownerHubHost) push(a.ownerHubHost); // legacy single-host field
|
|
81
|
+
return out;
|
|
68
82
|
} catch {
|
|
69
|
-
return
|
|
83
|
+
return [];
|
|
70
84
|
}
|
|
71
85
|
}
|
|
72
86
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
87
|
+
function ownerHubHosts() {
|
|
88
|
+
return _readHosts();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// The primary "owner's own hub home" — used to build an "open my hub at project X"
|
|
92
|
+
// URL. First host recorded via a grant (grantUrl); falls back to any known host.
|
|
93
|
+
function ownerHubHost() {
|
|
94
|
+
try {
|
|
95
|
+
const home = normHubHost(readGlobalConfig(GLOBAL_JSON)?.hubAuth?.ownerHubHome || "");
|
|
96
|
+
if (home) return home;
|
|
97
|
+
} catch {}
|
|
98
|
+
return _readHosts()[0] || "";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Add hosts to the trusted set (only while a token is present). `setHome` marks
|
|
102
|
+
// the first one as the primary home (grants do this; the instances bulk load
|
|
103
|
+
// doesn't, so a node subdomain never becomes "my hub home").
|
|
104
|
+
function _addHosts(hosts, setHome) {
|
|
105
|
+
const norm = (Array.isArray(hosts) ? hosts : [hosts]).map(normHubHost).filter(Boolean);
|
|
106
|
+
if (!norm.length) return { ok: false, added: 0 };
|
|
107
|
+
const have = new Set(_readHosts());
|
|
108
|
+
const add = norm.filter((h) => !have.has(h));
|
|
109
|
+
let homeToSet = "";
|
|
78
110
|
try {
|
|
79
111
|
updateGlobalConfig(GLOBAL_JSON, (c) => {
|
|
80
|
-
if (c && c.hubAuth && c.hubAuth.token)
|
|
112
|
+
if (c && c.hubAuth && c.hubAuth.token) {
|
|
113
|
+
const cur = Array.isArray(c.hubAuth.ownerHubHosts) ? c.hubAuth.ownerHubHosts.slice() : [];
|
|
114
|
+
const s = new Set(cur);
|
|
115
|
+
if (c.hubAuth.ownerHubHost) {
|
|
116
|
+
const legacy = normHubHost(c.hubAuth.ownerHubHost);
|
|
117
|
+
if (legacy) s.add(legacy);
|
|
118
|
+
delete c.hubAuth.ownerHubHost; // migrate
|
|
119
|
+
}
|
|
120
|
+
norm.forEach((h) => s.add(h));
|
|
121
|
+
c.hubAuth.ownerHubHosts = Array.from(s);
|
|
122
|
+
if (setHome && !c.hubAuth.ownerHubHome) {
|
|
123
|
+
c.hubAuth.ownerHubHome = norm[0];
|
|
124
|
+
homeToSet = norm[0];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
81
127
|
return c;
|
|
82
128
|
});
|
|
83
129
|
} catch {
|
|
84
|
-
return { ok: false };
|
|
130
|
+
return { ok: false, added: 0 };
|
|
131
|
+
}
|
|
132
|
+
if (add.length || homeToSet) {
|
|
133
|
+
_audit({
|
|
134
|
+
kind: "auth",
|
|
135
|
+
gate: "owner-hub",
|
|
136
|
+
host: (add.length ? add : norm).join(","),
|
|
137
|
+
decision: setHome ? "record" : "record-bulk",
|
|
138
|
+
});
|
|
85
139
|
}
|
|
86
|
-
|
|
87
|
-
|
|
140
|
+
return { ok: true, added: add.length };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// A single host proven to be the owner's (a grant). Also becomes the primary home.
|
|
144
|
+
function recordOwnerHubHost(input) {
|
|
145
|
+
const host = normHubHost(input);
|
|
146
|
+
if (!host) return { ok: false };
|
|
147
|
+
const r = _addHosts([host], true);
|
|
148
|
+
return r.ok ? { ok: true, host } : { ok: false };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Bulk: every host from the authenticated instances() list — all the owner's own
|
|
152
|
+
// nodes. Does NOT change the primary home.
|
|
153
|
+
function recordOwnerHubHosts(list) {
|
|
154
|
+
return _addHosts(list, false);
|
|
88
155
|
}
|
|
89
156
|
|
|
90
157
|
function clearOwnerHubHost() {
|
|
91
158
|
try {
|
|
92
159
|
updateGlobalConfig(GLOBAL_JSON, (c) => {
|
|
93
|
-
if (c && c.hubAuth)
|
|
160
|
+
if (c && c.hubAuth) {
|
|
161
|
+
delete c.hubAuth.ownerHubHost;
|
|
162
|
+
delete c.hubAuth.ownerHubHome;
|
|
163
|
+
delete c.hubAuth.ownerHubHosts;
|
|
164
|
+
}
|
|
94
165
|
return c;
|
|
95
166
|
});
|
|
96
167
|
} catch {}
|
|
97
168
|
}
|
|
98
169
|
|
|
99
|
-
// The gate: is this origin the owner's own hub
|
|
100
|
-
//
|
|
170
|
+
// The gate: is this origin one of the owner's own hub hosts (and are we still
|
|
171
|
+
// logged in)? Suffix-locked AND matched against the captured set.
|
|
101
172
|
function isOwnerHubOrigin(originOrUrl) {
|
|
102
173
|
if (!loggedIn()) return false;
|
|
103
|
-
const own = ownerHubHost();
|
|
104
|
-
if (!own) return false;
|
|
105
174
|
const host = normHubHost(originOrUrl);
|
|
106
|
-
|
|
175
|
+
if (!host) return false;
|
|
176
|
+
return _readHosts().includes(host);
|
|
107
177
|
}
|
|
108
178
|
|
|
109
179
|
module.exports = {
|
|
110
180
|
HUB_SUFFIX,
|
|
111
181
|
normHubHost,
|
|
112
182
|
ownerHubHost,
|
|
183
|
+
ownerHubHosts,
|
|
113
184
|
recordOwnerHubHost,
|
|
185
|
+
recordOwnerHubHosts,
|
|
114
186
|
clearOwnerHubHost,
|
|
115
187
|
isOwnerHubOrigin,
|
|
116
188
|
};
|
|
@@ -79,6 +79,8 @@ test("hub-client records the owner hub host on a grant and clears it on logout",
|
|
|
79
79
|
src.indexOf("function clearAuth") + 260
|
|
80
80
|
);
|
|
81
81
|
assert.match(clearAuth, /hubTrust\.clearOwnerHubHost\(\)/);
|
|
82
|
+
// instances() trusts every returned node host (each is the owner's own tenant).
|
|
83
|
+
assert.match(src, /hubTrust\.recordOwnerHubHosts\(out\.map\(\(i\) => i\.host\)\)/);
|
|
82
84
|
});
|
|
83
85
|
|
|
84
86
|
test("hub-trust: owner host is suffix-locked to <tenant>.hub.cicy-ai.com", () => {
|
|
@@ -114,12 +116,25 @@ test("hub-trust: owner host is suffix-locked to <tenant>.hub.cicy-ai.com", () =>
|
|
|
114
116
|
});
|
|
115
117
|
assert.equal(t.recordOwnerHubHost("https://limeng.hub.cicy-ai.com/").ok, true);
|
|
116
118
|
assert.equal(t.isOwnerHubOrigin("https://limeng.hub.cicy-ai.com/#/project/9"), true);
|
|
117
|
-
assert.equal(t.isOwnerHubOrigin("https://other.hub.cicy-ai.com/"), false); //
|
|
119
|
+
assert.equal(t.isOwnerHubOrigin("https://other.hub.cicy-ai.com/"), false); // not recorded → not trusted
|
|
118
120
|
assert.equal(t.recordOwnerHubHost("hub.cicy-ai.com").ok, false); // can't record bare suffix
|
|
119
121
|
|
|
120
|
-
//
|
|
122
|
+
// Trust is a SET, not a wildcard: each of the owner's node subdomains is
|
|
123
|
+
// trusted once the authenticated instances() list records it — and only then.
|
|
124
|
+
t.recordOwnerHubHosts([
|
|
125
|
+
"xs-1001.hub.cicy-ai.com",
|
|
126
|
+
"https://xs-2002.hub.cicy-ai.com/x",
|
|
127
|
+
"hub.cicy-ai.com", // dropped (bare suffix)
|
|
128
|
+
]);
|
|
129
|
+
assert.equal(t.isOwnerHubOrigin("https://xs-1001.hub.cicy-ai.com/"), true);
|
|
130
|
+
assert.equal(t.isOwnerHubOrigin("https://xs-2002.hub.cicy-ai.com/#/y"), true);
|
|
131
|
+
assert.equal(t.isOwnerHubOrigin("https://xs-9999.hub.cicy-ai.com/"), false); // never recorded
|
|
132
|
+
assert.equal(t.ownerHubHost(), "limeng.hub.cicy-ai.com"); // home stays the first grant, not a node
|
|
133
|
+
|
|
134
|
+
// Logout clears the whole set.
|
|
121
135
|
t.clearOwnerHubHost();
|
|
122
136
|
assert.equal(t.isOwnerHubOrigin("https://limeng.hub.cicy-ai.com/"), false);
|
|
137
|
+
assert.equal(t.isOwnerHubOrigin("https://xs-1001.hub.cicy-ai.com/"), false);
|
|
123
138
|
} finally {
|
|
124
139
|
process.env.HOME = prevHome;
|
|
125
140
|
if (prevUp === undefined) delete process.env.USERPROFILE;
|