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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cicy-desktop",
3
- "version": "2.1.327",
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.595",
148
- "cicy-code-darwin-arm64": "2.3.595",
149
- "cicy-code-linux-x64": "2.3.595",
150
- "cicy-code-linux-arm64": "2.3.595",
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
 
@@ -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 ONE origin a hub-logged-in desktop treats as "its owner
5
- // commanding it", so rpc_call from the operator's own ws-hub control surface can
6
- // drive dangerous tools (exec_*/file_*/electron_inject) on this machine WITHOUT a
7
- // per-call consent modal that nobody is present to click.
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 (deliberately the narrowest that still gives fleet control):
10
- // • ALL teams under the same login email share ONE origin
11
- // https://<tenant>.hub.cicy-ai.com (teams/projects are hash paths under it),
12
- // confirmed across the fleet. So owner-trust is a single hub host, not a set.
13
- // • That host is captured ONLY from an authenticated source: a hub grant minted
14
- // with THIS desktop's own hubAuth token (hub-client.grantUrl / hub:open).
15
- // Minting a grant proves the host belongs to this desktop's owner email
16
- // no origin-string guessing, no email→slug derivation.
17
- // Persisted under hubAuth so it survives restart (the deadlock repro was "also
18
- // after restart"); cleared the moment the desktop logs out.
19
- // Suffix-locked to `.hub.cicy-ai.com`: even a corrupted store can't promote an
20
- // arbitrary origin to owner trust.
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
- function ownerHubHost() {
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
- return normHubHost(readGlobalConfig(GLOBAL_JSON)?.hubAuth?.ownerHubHost || "");
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
- // Record the hub host proven to be the owner's (called on a successful grant).
74
- function recordOwnerHubHost(input) {
75
- const host = normHubHost(input);
76
- if (!host) return { ok: false };
77
- if (ownerHubHost() === host) return { ok: true, host };
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) c.hubAuth.ownerHubHost = host;
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
- _audit({ kind: "auth", gate: "owner-hub", host, decision: "record" });
87
- return { ok: true, host };
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) delete c.hubAuth.ownerHubHost;
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 control surface (and are we still
100
- // its owner)? Suffix-locked AND matched against the captured host.
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
- return !!host && host === own;
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); // different tenant
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
- // Logout clears trust.
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;