grix-connector 4.2.3 → 4.2.4

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 (40) hide show
  1. package/dist/adapter/claude/claude-adapter.js +18 -17
  2. package/dist/adapter/claude/claude-bridge-server.js +1 -1
  3. package/dist/adapter/claude/claude-tools.js +1 -1
  4. package/dist/adapter/claude/claude-worker-client.js +1 -1
  5. package/dist/adapter/claude/mcp-http-launcher.js +2 -2
  6. package/dist/adapter/claude/result-timeout.js +1 -1
  7. package/dist/assets/dsh-bridge/{grix-dsh-bridge-4.2.3.tgz → grix-dsh-bridge-4.2.4.tgz} +0 -0
  8. package/dist/assets/dsh-bridge/manifest.json +4 -4
  9. package/dist/core/access/allowlist-store.js +1 -1
  10. package/dist/core/file-ops/list-files.js +1 -1
  11. package/dist/default-skills/grix-access-control/SKILL.md +1 -1
  12. package/dist/default-skills/grix-admin/SKILL.md +110 -155
  13. package/dist/default-skills/grix-admin/references/api-contract.md +23 -12
  14. package/dist/default-skills/grix-agent-dispatch/SKILL.md +152 -188
  15. package/dist/default-skills/grix-audit-data/SKILL.md +1 -1
  16. package/dist/default-skills/grix-chat-state/SKILL.md +26 -43
  17. package/dist/default-skills/grix-egg/SKILL.md +1 -1
  18. package/dist/default-skills/grix-group/SKILL.md +1 -1
  19. package/dist/default-skills/grix-owner-relay/SKILL.md +30 -64
  20. package/dist/default-skills/grix-query/SKILL.md +1 -1
  21. package/dist/default-skills/grix-skill-library/SKILL.md +52 -0
  22. package/dist/default-skills/grix-widget-visitor-ban/SKILL.md +1 -1
  23. package/dist/default-skills/message-send/SKILL.md +1 -1
  24. package/dist/default-skills/message-unsend/SKILL.md +1 -1
  25. package/dist/default-skills/tailnet-file-share/SKILL.md +38 -72
  26. package/dist/default-skills/tailnet-file-share/references/certificate-trust.md +64 -0
  27. package/dist/log.js +2 -2
  28. package/dist/mcp/stream-http/config.js +1 -1
  29. package/dist/mcp/stream-http/connection-binding.js +1 -1
  30. package/dist/mcp/stream-http/security.js +1 -1
  31. package/dist/mcp/stream-http/tool-executor.js +1 -1
  32. package/dist/mcp/stream-http/tool-registry.js +1 -1
  33. package/dist/mcp/stream-http/tool-schemas.js +1 -1
  34. package/openclaw-plugin/skills/grix-admin/SKILL.md +122 -156
  35. package/openclaw-plugin/skills/grix-group/SKILL.md +52 -159
  36. package/openclaw-plugin/skills/grix-query/SKILL.md +54 -248
  37. package/openclaw-plugin/skills/grix-update/SKILL.md +70 -304
  38. package/openclaw-plugin/skills/message-send/SKILL.md +41 -172
  39. package/openclaw-plugin/skills/message-unsend/SKILL.md +29 -170
  40. package/package.json +1 -1
@@ -1,84 +1,50 @@
1
1
  ---
2
2
  name: grix-owner-relay
3
3
  description: Act on the owner's behalf in a session — send a message as the owner (`grix_session_send`), or call the owner into the current session for a voice talk/approval (`grix_call_owner`). Trigger when the user asks to speak as the owner in a session, or when you need to reach the owner to discuss or get approval. Dispatch callbacks follow the grix-agent-dispatch skill procedure `report_dispatch_result` (not a tool; it formats `[dispatch-result]` and calls this tool).
4
- trigger: 当需要以 owner 身份在某会话发言、或把 owner 叫进当前会话语音沟通/审批时
4
+ trigger: When you need to speak in a session as the owner, or call the owner into the current session for a voice conversation / approval
5
5
  ---
6
6
 
7
7
  # Grix Owner Relay
8
8
 
9
- Interact with sessions on the owner's behalf, or pull the owner in.
9
+ Two tools that involve the owner directly.
10
10
 
11
11
  ## Speak as the owner — `grix_session_send`
12
12
 
13
- Send a message into a session **as the owner** — it shows up as if the owner
14
- themselves sent it, **not** as you (the agent).
13
+ Sends a message into a session **as the owner** — it appears as the owner's
14
+ own words, **not** as you (the agent).
15
15
 
16
- - `session_id` (required) — target session ID.
17
- - `content` (required) message text to send as the owner (max 10000 chars).
18
- - `quoted_message_id` (optional) — message ID in the **same** target session to
19
- quote/reply to. Required for dispatch callbacks so the dispatcher is
20
- quote-woken.
16
+ - `session_id` (required) — target session ID. The owner must be a member of
17
+ it; otherwise the send is rejected with an owner-not-member error.
18
+ - `content` (required) — message text (max 10000 chars).
19
+ - `quoted_message_id` (optional) ID of a message in the **same** target
20
+ session to quote/reply to.
21
21
 
22
- ### When to use it
22
+ Requires the **Send as Owner** permission scope. If the call is rejected for a
23
+ missing permission or non-membership, surface the error as-is so the owner can
24
+ fix it; never work around it with `grix_message_send`. Your own membership in
25
+ the target session is not checked and is irrelevant.
23
26
 
24
- To relay on the owner's behalf into a session where the message must appear as
25
- the owner's own words. Typical case: you were dispatched to work somewhere and
26
- need to drop a note to the owner (or to others) in a session of theirs.
27
+ ### Use it only when the message must appear as the owner's
27
28
 
28
- **First-class use case: dispatch callback.** When you were dispatched via
29
- `grix-agent-dispatch`, follow the skill procedure `report_dispatch_result` in
30
- that skill (exactly 6 parameters; **not** a tool name). It formats `content`
31
- as **only** the `[dispatch-result]` wire block (no `@` line), and calls this
32
- tool with `session_id` = `callback_session_id` and
33
- `quoted_message_id` = the dispatcher anchor from the task pointer. Do not
34
- hand-roll the wire template in the task text; if you must call this tool
35
- directly for a dispatch callback, still send **only** that block plus the
36
- quote id no extra instructions, explanations, or requests outside the
37
- block.
38
-
39
- **Dispatch callbacks always go through this tool, even when you are a member
40
- of the callback session.** A message sent as yourself cannot quote-wake the
41
- dispatcher agent, so membership is irrelevant — never fall back to
42
- `grix_message_send` for a callback. If the call is rejected for missing
43
- permission, surface the error as-is so the owner can grant the scope; do not
44
- work around it.
45
-
46
- ### Before you call it, make sure
47
-
48
- 1. You genuinely want to **impersonate the owner**, not speak as yourself
49
- (dispatch callbacks are exactly such a case).
50
- 2. The owner is a member of the target session (otherwise the send is
51
- rejected with an owner-not-member error — surface it, don't retry blindly).
52
-
53
- ### Never use it for
54
-
55
- - ❌ **Sending your own ordinary reply in a conversation.** Reply normally
56
- instead (or use `grix_message_send` to send as yourself). Using
57
- `grix_session_send` for ordinary chatter makes *your* answer appear as the
58
- *owner's* words. The one exception is the structured dispatch callback
59
- (`report_dispatch_result`), which must always go through this tool.
60
- - ❌ As a generic substitute for sending a message as yourself — use
61
- `grix_message_send` for that.
62
-
63
- To send as yourself (the agent), use the `message-send` skill
64
- (`grix_message_send`).
29
+ - Dispatch callbacks: the `report_dispatch_result` procedure in
30
+ `grix-agent-dispatch` (a skill procedure with exactly 6 parameters, **not** a
31
+ tool name) builds a `[dispatch-result]` block and sends it through this
32
+ tool with `quoted_message_id` set and no `@` line. That skill owns the
33
+ format; follow it there.
34
+ - Relaying a note the owner asked you to send under their name.
35
+ - Your own ordinary replies reply normally, or use `grix_message_send`
36
+ (skill `message-send`) to send as yourself. Using this tool for ordinary
37
+ chatter makes *your* answer appear as the *owner's* words.
65
38
 
66
39
  ## Call the owner in — `grix_call_owner`
67
40
 
68
- Bring the owner into a session for a voice conversation — use this when you need
69
- to discuss something or get an approval/review during your work. It sends the
70
- owner an offline notification; tapping it lands them in the conversation and
71
- auto-starts a voice-brain call.
41
+ Brings the owner into a session for a voice conversation — use it when you
42
+ need to discuss something or get an approval/review during your work. The
43
+ owner receives an offline notification; tapping it lands them in the
44
+ conversation and auto-starts a voice-brain call.
72
45
 
73
46
  - `session_id` (required) — the session to call the owner into.
74
47
 
75
- ## Rules
76
-
77
- 1. `grix_session_send` requires the owner to be a member of the target session,
78
- plus the **Send as Owner** permission scope. Your own membership in the
79
- target session is irrelevant and is not checked. If the call is rejected for
80
- missing permission, surface the error as-is — the owner can grant the scope;
81
- never work around it with `grix_message_send`.
82
- 2. `grix_call_owner` requires the owner to have configured a voice brain and is
83
- rate-limited per session. Use it only when you genuinely need the owner, not
84
- as a routine notification.
48
+ Requires the owner to have configured a voice brain, and is rate-limited per
49
+ session. Use it only when you genuinely need the owner, not as a routine
50
+ notification.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: grix-query
3
3
  description: Use the typed `grix_query` tool for Grix contact lookup, keyword search, session search, favorited-session search, and session message history lookup. Trigger when users ask to find contacts, search conversations, list visible or favorited sessions, or inspect recent messages in a known session.
4
- trigger: 当用户要查找联系人、搜索会话、列出可见会话或收藏会话、或查看某个已知会话的历史消息时
4
+ trigger: When the user wants to find contacts, search sessions, list visible or favorited sessions, or read message history of a known session
5
5
  ---
6
6
 
7
7
  # Grix Query
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: grix-skill-library
3
+ description: Manage the owner's custom skill library on the Grix platform with `grix_skill_get` (list or read a skill's SKILL.md) and `grix_skill_set` (create, overwrite, or delete a skill by name). The library syncs to every machine the owner runs an agent on. Trigger when the user asks to save something as a reusable skill, list or show their skills, edit an existing skill, or delete one.
4
+ trigger: When the user wants to list, read, create, update, or delete one of their custom Grix skills, or turn a procedure into a reusable skill
5
+ ---
6
+
7
+ # Grix Skill Library
8
+
9
+ The owner's custom skills live in the Grix platform and auto-sync to a
10
+ per-machine `grix/skills` directory on every host where the owner runs an
11
+ agent. These two tools manage that library only; whether and when an agent
12
+ loads a synced skill is up to the owner (system prompt or conversation).
13
+
14
+ ## Tools
15
+
16
+ - `grix_skill_get` — `name` (optional). With a name: returns that skill's full
17
+ `SKILL.md` text. Without: lists the library (names + versions).
18
+ - `grix_skill_set` — `name` (required, max 100 chars, unique per owner; also
19
+ the synced directory name) and `content` (required, full `SKILL.md` text,
20
+ max 256 KB). An existing name is overwritten. An empty `content` string
21
+ **deletes** the skill.
22
+
23
+ ## Skill format
24
+
25
+ `content` is a standard `SKILL.md`: YAML frontmatter with `name` (matching
26
+ the `name` argument) and a one-line `description` that says what the skill
27
+ does and when to use it, then a Markdown body with the procedure — inputs,
28
+ steps, rules, and how to report. Write it for an agent that has no other
29
+ context. Keep it focused on one job; do not embed secrets, machine-specific
30
+ paths, or credentials.
31
+
32
+ ## Workflow
33
+
34
+ 1. **Create**: confirm the name and what the skill should do; draft the
35
+ `SKILL.md`; call `grix_skill_set`. Before overwriting, call
36
+ `grix_skill_get` with the name to check whether it already exists and
37
+ tell the user if it does.
38
+ 2. **Edit**: `grix_skill_get` the current content first, apply the change to
39
+ that text, then `grix_skill_set` the full revised text — never send a
40
+ partial body.
41
+ 3. **Delete**: confirm with the user, then `grix_skill_set` with the name and
42
+ `content: ""`.
43
+ 4. **List / show**: `grix_skill_get` without a name, or with the name.
44
+
45
+ ## Rules
46
+
47
+ 1. Never guess a skill name — list first if unsure.
48
+ 2. Overwrite and delete are irreversible; confirm before either when the
49
+ user did not explicitly ask for it.
50
+ 3. After a successful set, tell the user the skill is saved and will sync to
51
+ their machines; do not paste the whole content back unless asked.
52
+ 4. On a permission or scope error, report it as-is; do not retry.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: grix-widget-visitor-ban
3
3
  description: Ban a widget visitor session with the typed `grix_widget_visitor_ban` tool — blocks the visitor's chat session and automatically adds the session's most recent init IP to the owner's global IP ban list (7-day expiry by default). Trigger when the owner asks to ban, block, or kick an abusive/spamming widget visitor.
4
- trigger: 当用户要封禁、拉黑或踢掉某个骚扰/滥用的 widget 访客会话时
4
+ trigger: When the owner wants to ban, block, or kick an abusive or spamming widget visitor session
5
5
  ---
6
6
 
7
7
  # Grix Widget Visitor Ban
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: message-send
3
3
  description: "Send a message into a specific Grix session by session ID, including cross-session and proactive sends. For replying to the current event use the reply/complete tools instead. Trigger words: send DM, DM, send message, notify, message another session."
4
- trigger: 当用户要主动给某个指定会话发消息、跨会话发送、或通知另一个会话时
4
+ trigger: When the user wants to proactively message a specific session, send across sessions, or notify another session
5
5
  ---
6
6
 
7
7
  # Message Send
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: message-unsend
3
3
  description: "Silently recall/unsend an already-sent message in a Grix session. After execution, end immediately without replying any confirmation text. Trigger words: recall, unsend, delete message, withdraw message."
4
- trigger: 当用户要撤回、收回、删除一条已经发出的消息时
4
+ trigger: When the user wants to recall, retract, or delete a message that was already sent
5
5
  ---
6
6
 
7
7
  # Message Unsend
@@ -1,88 +1,54 @@
1
1
  ---
2
2
  name: tailnet-file-share
3
- description: Turn a local file into a tailnet link images render as inline previews, everything else gets a download link. Trigger when the user asks to view, send, share, download, or export any file on this machine.
4
- trigger: 当用户要求查看、发送、分享、下载、导出本机上的任何文件时
3
+ description: Deliver a local file to the useras a native chat attachment (`grix_file_upload`) or as a tailnet download/preview link (`grix_file_link`). Trigger when the user asks to view, send, share, download, or export any file on this machine.
4
+ trigger: When the user asks to view, send, share, download, or export any file on this machine
5
5
  ---
6
6
 
7
7
  # Tailnet File Share
8
8
 
9
- When the user wants to view or obtain a file on this machine (images, logs, reports, build artifacts, exported data, etc.), you must:
9
+ When the user wants a file that lives on this machine (image, log, report,
10
+ build artifact, exported data, …), never paste its path or contents. Pick one
11
+ of two tools and hand over the result.
10
12
 
11
- 1. Call the `grix_file_link` tool with the file's absolute path.
12
- 2. Copy the tool's `markdown` field into your reply verbatim — do not modify it.
13
- - Image files (jpg/png/gif/webp/svg, etc.): the tool returns `![filename](url)`, which renders as an inline image preview in chat.
14
- - Other files: the tool returns `[filename](url)`, which the user clicks to download.
15
- 3. Do not print the raw file path, and do not paste or paraphrase the file contents.
16
- 4. The link is bound to this machine's tailnet-internal address and is unreachable from the public internet. The user can reopen it any time, so under normal circumstances there is no expiry to worry about and none to mention — just send the link.
13
+ ## 1. Choose the tool
17
14
 
18
- ## When to use `grix_file_upload` (native attachment) instead
15
+ | Scenario | Tool |
16
+ |---|---|
17
+ | The file should appear as a proper attachment in the chat — especially images or videos the user will look at inline; ≤ 50 MB | `grix_file_upload` |
18
+ | File > 50 MB, a clickable download link is enough, or it is an arbitrary local artifact | `grix_file_link` |
19
19
 
20
- Besides `grix_file_link` there is a `grix_file_upload` tool: it uploads a local
21
- file to the Grix platform, where it appears in the target session as a **native
22
- attachment message** (images/videos display inline in chat no tailnet, no
23
- certificate install needed). It supports images, videos, documents, and
24
- archives, up to 50 MB per file. Choose by scenario:
20
+ `grix_file_upload` uploads to the Grix platform; the attachment renders
21
+ natively (images/videos inline) with no tailnet or certificate involved.
22
+ Supports images, videos, documents, archives up to 50 MB per file.
25
23
 
26
- - The file should be a proper attachment in the chat (especially sending images or videos for the user to look at) → use `grix_file_upload`.
27
- - The file is large (>50 MB), a clickable download link is all that's needed, or it's an arbitrary local-path artifact → use `grix_file_link`.
24
+ `grix_file_link` serves the file over HTTPS from this machine's tailnet
25
+ address. The link is unreachable from the public internet, stays valid
26
+ indefinitely, and the user can reopen it any time — do not mention expiry.
28
27
 
29
- ## HTTPS and the one-time trust-certificate install
28
+ ## 2. Call it and relay the result
30
29
 
31
- Download links are HTTPS (`https://…`), signed on the fly by a self-signed CA
32
- built into this machine with a 10-year validity. The tool result also returns
33
- `ca_install_url` the install link for the root CA certificate.
30
+ - `grix_file_upload`: `file_path` (absolute) and `session_id` (the current
31
+ chat's session unless the user names another); optional `caption` and
32
+ `reply_to_message_id`. The tool delivers the attachment do not also send
33
+ a link.
34
+ - `grix_file_link`: `file_path` (absolute); leave `ttl_ms` unset. Copy the
35
+ tool's `markdown` field into your reply **verbatim**:
36
+ - images (jpg/png/gif/webp/svg …) come back as `![filename](url)` and
37
+ render as an inline preview;
38
+ - everything else comes back as `[filename](url)` for download.
34
39
 
35
- - **The first time you send a file link to a given user** (or whenever they report "the link won't open / certificate not trusted / not secure"), send them `ca_install_url` as the "install trust certificate" link along with the steps below.
36
- - Each device only needs this **once**; afterwards, for 10 years, every download link from this machine is warning-free, and no reinstall is needed even if the tailnet IP changes.
40
+ Do not print the raw file path and do not paraphrase the file contents.
37
41
 
38
- Install guidance to send the user (pick the one matching their device):
42
+ If `grix_file_link` fails (e.g. Tailscale is not connected), tell the user the
43
+ local file path so they can retrieve it themselves.
39
44
 
40
- - **iPhone / iPad**: open `ca_install_url` in Safari — it prompts "This website is trying to download a configuration profile" → Allow → a "Profile Downloaded" entry appears at the top of Settings; tap it to install → then go to Settings → General → About → Certificate Trust Settings and turn the switch on for that certificate (enable full trust). This step is mandatory, otherwise the system still won't trust it. (iOS must use Safari; other browsers won't trigger profile installation. The server already returns a `.mobileconfig` based on the device, so no manual parameters are needed.)
41
- - **Mac**: click the link to download the `.crt` → double-click to open it in Keychain Access → find the certificate → double-click → expand "Trust" → set "When using this certificate" to "Always Trust".
42
- - **Android**: tap `ca_install_url` to download the `.crt` → Settings → Security → Encryption & credentials → Install a certificate → CA certificate, and pick the downloaded file (recent Android versions do not support one-tap install from the browser; the Settings route is required).
43
- - **Windows**: double-click the `.crt` → "Install Certificate" → choose the store location → "Place all certificates in the following store" → select "Trusted Root Certification Authorities".
45
+ ## 3. If the link does not open (`grix_file_link` only)
44
46
 
45
- If the `grix_file_link` call fails (e.g. Tailscale is not connected), tell the
46
- user the local file path and let them retrieve it themselves.
47
-
48
- ## Scope (important): this CA only covers `grix_file_link`'s own service
49
-
50
- The built-in CA above and the certificates it issues **only make the download
51
- service started by `grix_file_link` (this machine's tailnet address) trusted by
52
- the device**. It does not cover — and cannot fix — other services the user
53
- starts themselves.
54
-
55
- When the user has **started their own HTTPS service on the tailnet** (a
56
- self-hosted site / API / media server, etc.) and the browser reports a
57
- certificate error, be clear that this is a separate matter:
58
-
59
- - A device only trusts "certificates issued by a CA it has personally installed". A self-hosted service uses its own CA (e.g. mkcert), which the device never installed, so it reports `not trusted / impersonation / NET::ERR_CERT_AUTHORITY_INVALID` — this has nothing to do with whether the Grix CA is installed.
60
- - There are only two ways to make a self-hosted service trusted:
61
- 1. Have that service sign its certificate with a CA the device already trusts, and keep the leaf certificate's validity **≤ 398 days** (otherwise, even with the CA trusted, it reports `NET::ERR_CERT_VALIDITY_TOO_LONG`);
62
- 2. Or simply don't have the user run their own HTTPS — share the files via `grix_file_link` instead, served by the built-in service, automatically trusted and with compliant validity.
63
- - Note: the Grix built-in CA's **private key stays only on the machine running the connector and is never handed out** (`ca_install_url` serves the public certificate only, not the private key). So only services **on the same machine as the connector** can borrow this CA for signing; services on other machines must bring their own CA and install it once per device.
64
-
65
- In short: if it can be sent via `grix_file_link`, send it that way — it's the
66
- least trouble. Certificate trust for self-hosted services is outside this
67
- skill's responsibility and must be solved on that service's side.
68
-
69
- ## Two trust paths: inside the Grix app vs. the system browser (diagnose this first)
70
-
71
- The same link opened **in the Grix app** and **in a system browser (Safari /
72
- Chrome)** goes through two entirely independent certificate-trust mechanisms,
73
- with different errors and different fixes. When a user reports "won't open /
74
- certificate error", first find out which path they were on.
75
-
76
- - **Inside the Grix app** (image previews, in-app downloads, etc. use the app's own network stack): the app has a built-in trust rule — if the link is a tailnet address (`100.64.0.0/10` range) and the certificate's issuer name contains `Grix Tailnet Local CA`, it is allowed through, and **the user does not need to install any certificate on the device**. So `grix_file_link` links generally work out of the box in the app with zero install. If it still won't open in the app, it's most likely not a tailnet IP, or the issuer name doesn't match — not a "certificate not installed" problem.
77
- - **System browser (Safari / Chrome)**: this goes through the phone's / computer's system trust store, which has nothing to do with the app's rule. The root CA must be installed as described in *HTTPS and the one-time trust-certificate install* above (on iOS, install the `.mobileconfig` and enable full trust; on Android, go through Settings; and so on). `NET::ERR_CERT_AUTHORITY_INVALID`, "impersonation", and `NET::ERR_CERT_VALIDITY_TOO_LONG` in the user's screenshots are almost always this path — guide them to install the CA.
78
-
79
- The same distinction applies to **self-hosted services**, decided by where the user wants to view it:
80
-
81
- - Only needs to display **inside the Grix app**: the self-hosted service does not have to distribute a private key to the user, and no certificate install is needed — it only needs to ① be served on a tailnet IP, and ② have its CA's issuer name carry `Grix Tailnet Local CA`, and the app will trust it.
82
- - Needs to open in a **system browser**: the app's rule does not apply; that service's own CA must still be installed on each device (see *Scope* above).
83
-
84
- Note: the in-app rule is **a string match on the issuer name**, not a binding to
85
- a specific CA's public-key fingerprint. It is a convenience trade-off within a
86
- private tailnet only and **is not a hard security boundary** — anyone on the
87
- same tailnet can have the app trust them simply by naming their CA with the same
88
- prefix. Do not treat it as a trust root that stands up to an attacker.
47
+ Links open inside the Grix app with no setup. In a system browser
48
+ (Safari/Chrome) the device must trust this machine's root CA once: the tool
49
+ result includes `ca_install_url`. The first time you send a link to a user, or
50
+ whenever they report "won't open / certificate not trusted / not secure", send
51
+ `ca_install_url` with the per-device steps in
52
+ [references/certificate-trust.md](references/certificate-trust.md). That file
53
+ also explains why the CA does not cover the user's own self-hosted HTTPS
54
+ services.
@@ -0,0 +1,64 @@
1
+ # Certificate trust for `grix_file_link` links
2
+
3
+ Download links are HTTPS, signed on the fly by a self-signed root CA built
4
+ into this machine (10-year validity). The tool result returns
5
+ `ca_install_url`, the install link for that root CA's **public** certificate.
6
+
7
+ ## Two independent trust paths — diagnose this first
8
+
9
+ - **Inside the Grix app** (image previews, in-app downloads): the app has a
10
+ built-in rule — a link on a tailnet address (`100.64.0.0/10`) whose
11
+ certificate issuer name contains `Grix Tailnet Local CA` is allowed through.
12
+ No certificate install is needed. If a link still fails in the app, the
13
+ cause is a non-tailnet IP or a mismatched issuer name, not a missing
14
+ certificate.
15
+ - **System browser (Safari / Chrome)**: uses the device's system trust store,
16
+ which knows nothing about the app rule. The root CA must be installed once
17
+ per device. `NET::ERR_CERT_AUTHORITY_INVALID`, "impersonation", and
18
+ `NET::ERR_CERT_VALIDITY_TOO_LONG` in a user's screenshot are almost always
19
+ this path.
20
+
21
+ Note: the in-app rule is a string match on the issuer name, not a pin on a
22
+ specific CA key. It is a convenience inside a private tailnet, not a security
23
+ boundary.
24
+
25
+ ## One-time install per device (system browser path)
26
+
27
+ Each device needs this once; afterwards every link from this machine is
28
+ warning-free for 10 years, even if the tailnet IP changes. Send the step
29
+ matching the user's device:
30
+
31
+ - **iPhone / iPad**: open `ca_install_url` in **Safari** (other browsers do
32
+ not trigger profile install) → "This website is trying to download a
33
+ configuration profile" → Allow → Settings shows "Profile Downloaded" at the
34
+ top; tap it to install → then Settings → General → About → Certificate
35
+ Trust Settings → enable full trust for the certificate. The last step is
36
+ mandatory. The server returns a `.mobileconfig` automatically.
37
+ - **Mac**: download the `.crt` → double-click to open in Keychain Access →
38
+ find the certificate → double-click → expand "Trust" → set "When using this
39
+ certificate" to "Always Trust".
40
+ - **Android**: download the `.crt` → Settings → Security → Encryption &
41
+ credentials → Install a certificate → CA certificate → pick the file
42
+ (recent Android versions require the Settings route).
43
+ - **Windows**: double-click the `.crt` → "Install Certificate" → "Place all
44
+ certificates in the following store" → "Trusted Root Certification
45
+ Authorities".
46
+
47
+ ## Self-hosted HTTPS services are out of scope
48
+
49
+ The built-in CA only makes `grix_file_link`'s own download service trusted. A
50
+ service the user runs themselves (site / API / media server on the tailnet)
51
+ uses its own CA that the device never installed, so the browser reports
52
+ `not trusted / NET::ERR_CERT_AUTHORITY_INVALID` regardless of the Grix CA.
53
+
54
+ Ways to make such a service trusted:
55
+
56
+ 1. Sign its certificate with a CA the device already trusts, keeping the leaf
57
+ validity ≤ 398 days (longer triggers `NET::ERR_CERT_VALIDITY_TOO_LONG`).
58
+ The Grix CA's private key never leaves the machine running the connector
59
+ (`ca_install_url` serves only the public certificate), so only services on
60
+ that same machine could borrow it.
61
+ 2. For in-app viewing only: serve on a tailnet IP with an issuer name
62
+ containing `Grix Tailnet Local CA` — the app rule then applies, but system
63
+ browsers still need that service's CA installed.
64
+ 3. Simplest: share files through `grix_file_link` instead.
package/dist/log.js CHANGED
@@ -1,3 +1,3 @@
1
- import{createWriteStream as g,mkdirSync as l,existsSync as f}from"node:fs";import{join as i}from"node:path";import{homedir as m}from"node:os";const n=i(m(),".grix"),s={base:n,config:i(n,"config"),log:i(n,"log"),data:i(n,"data")};function S(){for(const o of Object.values(s))f(o)||l(o,{recursive:!0})}let a=null;function $(){const o=new Date().toISOString().slice(0,10),r=i(s.log,`grix-acp-${o}.log`);a=g(r,{flags:"a"})}function c(){return new Date().toISOString().slice(11,19)}const u={info(o,r,...t){const e=`${c()} [${o}] ${r}${t.length?" "+t.map(String).join(" "):""}`;console.log(e),a?.write(e+`
2
- `)},error(o,r,...t){const e=`${c()} [${o}] ERROR ${r}${t.length?" "+t.map(String).join(" "):""}`;console.error(e),a?.write(e+`
1
+ import{createWriteStream as g,mkdirSync as l,existsSync as f}from"node:fs";import{join as t}from"node:path";import{homedir as m}from"node:os";const i=t(m(),".grix"),s={base:i,config:t(i,"config"),log:t(i,"log"),data:t(i,"data")};function S(){for(const o of Object.values(s))f(o)||l(o,{recursive:!0})}let a=null;function $(){const o=new Date().toISOString().slice(0,10),r=t(s.log,`grix-acp-${o}.log`);a=g(r,{flags:"a"})}function c(){return new Date().toISOString().slice(11,19)}const u={info(o,r,...n){const e=`${c()} [${o}] ${r}${n.length?" "+n.map(String).join(" "):""}`;console.log(e),a?.write(e+`
2
+ `)},error(o,r,...n){const e=`${c()} [${o}] ERROR ${r}${n.length?" "+n.map(String).join(" "):""}`;console.error(e),a?.write(e+`
3
3
  `)}};export{s as GRIX_PATHS,S as ensureGrixDirs,$ as initLogger,u as log};
@@ -1 +1 @@
1
- import*as n from"node:net";const i={bind:"127.0.0.1",port:0,endpoint:"/mcp",sessionTimeoutMs:18e5,invokeTimeoutMs:3e4};function s(u){const e={bind:u?.bind??i.bind,port:u?.port??i.port,endpoint:u?.endpoint??i.endpoint,sessionTimeoutMs:u?.sessionTimeoutMs??i.sessionTimeoutMs,invokeTimeoutMs:u?.invokeTimeoutMs??i.invokeTimeoutMs,allowedOrigins:u?.allowedOrigins,allowedHosts:u?.allowedHosts};return t(e.bind),e.port!==0&&o(e.port),r(e.sessionTimeoutMs),e}function t(u){if(!u||!n.isIPv4(u)&&!n.isIPv6(u))throw new Error(`\u914D\u7F6E\u6821\u9A8C\u5931\u8D25: bind \u5730\u5740 "${u}" \u4E0D\u662F\u5408\u6CD5\u7684 IPv4 \u6216 IPv6 \u5730\u5740`)}function o(u){if(!Number.isInteger(u)||u<1||u>65535)throw new Error(`\u914D\u7F6E\u6821\u9A8C\u5931\u8D25: port \u503C ${u} \u4E0D\u5728\u5408\u6CD5\u8303\u56F4 1-65535 \u5185\u6216\u4E0D\u662F\u6574\u6570`)}function r(u){if(!Number.isInteger(u)||u<1e3||u>864e5)throw new Error(`\u914D\u7F6E\u6821\u9A8C\u5931\u8D25: session_timeout_ms \u503C ${u} \u4E0D\u5728\u5408\u6CD5\u8303\u56F4 1000-86400000 \u5185`)}export{s as createDefaultGatewayConfig};
1
+ import*as i from"node:net";const e={bind:"127.0.0.1",port:0,endpoint:"/mcp",sessionTimeoutMs:18e5,invokeTimeoutMs:3e4};function n(o){const u={bind:o?.bind??e.bind,port:o?.port??e.port,endpoint:o?.endpoint??e.endpoint,sessionTimeoutMs:o?.sessionTimeoutMs??e.sessionTimeoutMs,invokeTimeoutMs:o?.invokeTimeoutMs??e.invokeTimeoutMs,allowedOrigins:o?.allowedOrigins,allowedHosts:o?.allowedHosts};return s(u.bind),u.port!==0&&t(u.port),r(u.sessionTimeoutMs),u}function s(o){if(!o||!i.isIPv4(o)&&!i.isIPv6(o))throw new Error(`\u914D\u7F6E\u6821\u9A8C\u5931\u8D25: bind \u5730\u5740 "${o}" \u4E0D\u662F\u5408\u6CD5\u7684 IPv4 \u6216 IPv6 \u5730\u5740`)}function t(o){if(!Number.isInteger(o)||o<1||o>65535)throw new Error(`\u914D\u7F6E\u6821\u9A8C\u5931\u8D25: port \u503C ${o} \u4E0D\u5728\u5408\u6CD5\u8303\u56F4 1-65535 \u5185\u6216\u4E0D\u662F\u6574\u6570`)}function r(o){if(!Number.isInteger(o)||o<1e3||o>864e5)throw new Error(`\u914D\u7F6E\u6821\u9A8C\u5931\u8D25: session_timeout_ms \u503C ${o} \u4E0D\u5728\u5408\u6CD5\u8303\u56F4 1000-86400000 \u5185`)}export{n as createDefaultGatewayConfig};
@@ -1 +1 @@
1
- const a=3e4;class c{connectionManager;onDisconnected;bindings=new Map;constructor(n,i){this.connectionManager=n,this.onDisconnected=i}async bind(n,i){if(this.bindings.has(n))throw new Error(`Session ${n} is already bound to a connection`);const e=await this.connectWithTimeout(i),t=[],s=e.onDisconnected(()=>{this.removeBinding(n),this.onDisconnected(n)});return t.push(s),this.bindings.set(n,{sessionId:n,handle:e,subscriptions:t}),e}getHandle(n){return this.bindings.get(n)?.handle}unbind(n){const i=this.bindings.get(n);if(i){this.bindings.delete(n);for(const e of i.subscriptions)e();i.handle.disconnect()}}unbindAll(){const n=[...this.bindings.keys()];for(const i of n)this.unbind(i)}connectWithTimeout(n){return new Promise((i,e)=>{let t=!1;const s=setTimeout(()=>{t||(t=!0,e(new Error("Connection bind timeout after 30000ms")))},3e4);this.connectionManager.connect({agentId:n.agentId,apiKey:n.apiKey,url:n.wsUrl,clientType:n.clientType,capabilities:["agent_invoke"],adapterHint:`${n.clientType}/base`},{maxRetries:0}).then(o=>{t?o.disconnect():(t=!0,clearTimeout(s),i(o))}).catch(o=>{t||(t=!0,clearTimeout(s),e(o))})})}removeBinding(n){const i=this.bindings.get(n);if(i){this.bindings.delete(n);for(const e of i.subscriptions)e()}}}export{c as ConnectionBindingImpl};
1
+ const r=3e4;class c{connectionManager;onDisconnected;bindings=new Map;constructor(n,i){this.connectionManager=n,this.onDisconnected=i}async bind(n,i){if(this.bindings.has(n))throw new Error(`Session ${n} is already bound to a connection`);const e=await this.connectWithTimeout(i),t=[],s=e.onDisconnected(()=>{this.removeBinding(n),this.onDisconnected(n)});return t.push(s),this.bindings.set(n,{sessionId:n,handle:e,subscriptions:t}),e}getHandle(n){return this.bindings.get(n)?.handle}unbind(n){const i=this.bindings.get(n);if(i){this.bindings.delete(n);for(const e of i.subscriptions)e();i.handle.disconnect()}}unbindAll(){const n=[...this.bindings.keys()];for(const i of n)this.unbind(i)}connectWithTimeout(n){return new Promise((i,e)=>{let t=!1;const s=setTimeout(()=>{t||(t=!0,e(new Error("Connection bind timeout after 30000ms")))},3e4);this.connectionManager.connect({agentId:n.agentId,apiKey:n.apiKey,url:n.wsUrl,clientType:n.clientType,capabilities:["agent_invoke"],adapterHint:`${n.clientType}/base`},{maxRetries:0}).then(o=>{t?o.disconnect():(t=!0,clearTimeout(s),i(o))}).catch(o=>{t||(t=!0,clearTimeout(s),e(o))})})}removeBinding(n){const i=this.bindings.get(n);if(i){this.bindings.delete(n);for(const e of i.subscriptions)e()}}}export{c as ConnectionBindingImpl};
@@ -1 +1 @@
1
- function a(o){const e=new Set([`http://127.0.0.1:${o.serverPort}`,`http://localhost:${o.serverPort}`,...o.allowedOrigins]),t=new Set([`127.0.0.1:${o.serverPort}`,`localhost:${o.serverPort}`,...o.allowedHosts]);return{validateRequest(s){const r=i(s,e);if(!r.ok)return r;const n=l(s,t);return n.ok?{ok:!0}:n}}}function i(o,e){const t=o.headers.origin;return t?e.has(t)?{ok:!0}:{ok:!1,statusCode:403,message:`Origin not allowed: ${t}`}:{ok:!0}}function l(o,e){const t=o.headers.host;return t?e.has(t)?{ok:!0}:{ok:!1,statusCode:403,message:`Host not allowed: ${t}`}:{ok:!1,statusCode:403,message:"Missing Host header"}}export{a as createSecurityPolicy};
1
+ function a(t){const o=new Set([`http://127.0.0.1:${t.serverPort}`,`http://localhost:${t.serverPort}`,...t.allowedOrigins]),e=new Set([`127.0.0.1:${t.serverPort}`,`localhost:${t.serverPort}`,...t.allowedHosts]);return{validateRequest(s){const r=i(s,o);if(!r.ok)return r;const n=l(s,e);return n.ok?{ok:!0}:n}}}function i(t,o){const e=t.headers.origin;return e?o.has(e)?{ok:!0}:{ok:!1,statusCode:403,message:`Origin not allowed: ${e}`}:{ok:!0}}function l(t,o){const e=t.headers.host;return e?o.has(e)?{ok:!0}:{ok:!1,statusCode:403,message:`Host not allowed: ${e}`}:{ok:!1,statusCode:403,message:"Missing Host header"}}export{a as createSecurityPolicy};
@@ -1 +1 @@
1
- import{toolCallToInvoke as i}from"../../core/mcp/tools.js";import{ToolRegistryImpl as l}from"./tool-registry.js";import{validateToolArgs as a}from"./tool-schemas.js";import{isEventTool as p,executeEventTool as d}from"./event-tool-executor.js";class y{registry;constructor(){this.registry=new l}async execute(r,e,t,n){if(!this.registry.hasTool(e))return this.errorResult(`\u672A\u77E5\u5DE5\u5177: ${e}`);const s=a(e,t);if(!s.valid)return this.errorResult(`\u53C2\u6570\u6821\u9A8C\u5931\u8D25: ${s.error}`);if(r.status!=="ready")return this.errorResult(`\u8FDE\u63A5\u4E0D\u53EF\u7528: \u5F53\u524D\u72B6\u6001\u4E3A ${r.status}`);if(p(e))return this.executeEventTool(r,e,t);const o=i(e,t);try{const u=await r.agentInvoke(o.action,o.params,n);return this.normalizeResult(u)}catch(u){const c=u instanceof Error?u.message:String(u);return c.toLowerCase().includes("timeout")?this.errorResult(`\u8C03\u7528\u8D85\u65F6: ${c}`):this.errorResult(`\u8C03\u7528\u5931\u8D25: ${c}`)}}normalizeResult(r){if(r==null||typeof r!="object")return this.successResult(r??null);const e=r,t=typeof e.code=="number"?e.code:0;if(t===0){const s="data"in e?e.data:null;return this.successResult(s??null)}const n=typeof e.msg=="string"?e.msg:"\u672A\u77E5\u9519\u8BEF";return this.errorResult(`\u4E0A\u6E38\u9519\u8BEF [code=${t}]: ${n}`)}successResult(r){return{content:[{type:"text",text:JSON.stringify(r)}],isError:!1}}errorResult(r){return{content:[{type:"text",text:r}],isError:!0}}async executeEventTool(r,e,t){return e==="grix_access_control"?this.executeAccessControl(r,t):d(r,e,t)}async executeAccessControl(r,e){const t=String(e.action??""),n={pair_approve:"pair_approve",pair_deny:"pair_deny",allow_sender:"sender_allow",remove_sender:"sender_remove",set_policy:"policy_set"}[t];if(!n)return this.errorResult(`\u672A\u77E5 access_control action: ${t}`);const s={};e.code!=null&&(s.code=e.code),e.sender_id!=null&&(s.sender_id=e.sender_id),e.policy!=null&&(s.policy=e.policy);try{const o=await r.agentInvoke("claude_access_control",{verb:n,payload:s},3e4);return this.successResult(o)}catch(o){const u=o instanceof Error?o.message:String(o);return this.errorResult(`access_control \u8C03\u7528\u5931\u8D25: ${u}`)}}}export{y as ToolExecutorImpl};
1
+ import{toolCallToInvoke as i}from"../../core/mcp/tools.js";import{ToolRegistryImpl as l}from"./tool-registry.js";import{validateToolArgs as a}from"./tool-schemas.js";import{isEventTool as p,executeEventTool as d}from"./event-tool-executor.js";class y{registry;constructor(){this.registry=new l}async execute(t,e,r,n){if(!this.registry.hasTool(e))return this.errorResult(`\u672A\u77E5\u5DE5\u5177: ${e}`);const s=a(e,r);if(!s.valid)return this.errorResult(`\u53C2\u6570\u6821\u9A8C\u5931\u8D25: ${s.error}`);if(t.status!=="ready")return this.errorResult(`\u8FDE\u63A5\u4E0D\u53EF\u7528: \u5F53\u524D\u72B6\u6001\u4E3A ${t.status}`);if(p(e))return this.executeEventTool(t,e,r);const o=i(e,r);try{const u=await t.agentInvoke(o.action,o.params,n);return this.normalizeResult(u)}catch(u){const c=u instanceof Error?u.message:String(u);return c.toLowerCase().includes("timeout")?this.errorResult(`\u8C03\u7528\u8D85\u65F6: ${c}`):this.errorResult(`\u8C03\u7528\u5931\u8D25: ${c}`)}}normalizeResult(t){if(t==null||typeof t!="object")return this.successResult(t??null);const e=t,r=typeof e.code=="number"?e.code:0;if(r===0){const s="data"in e?e.data:null;return this.successResult(s??null)}const n=typeof e.msg=="string"?e.msg:"\u672A\u77E5\u9519\u8BEF";return this.errorResult(`\u4E0A\u6E38\u9519\u8BEF [code=${r}]: ${n}`)}successResult(t){return{content:[{type:"text",text:JSON.stringify(t)}],isError:!1}}errorResult(t){return{content:[{type:"text",text:t}],isError:!0}}async executeEventTool(t,e,r){return e==="grix_access_control"?this.executeAccessControl(t,r):d(t,e,r)}async executeAccessControl(t,e){const r=String(e.action??""),n={pair_approve:"pair_approve",pair_deny:"pair_deny",allow_sender:"sender_allow",remove_sender:"sender_remove",set_policy:"policy_set"}[r];if(!n)return this.errorResult(`\u672A\u77E5 access_control action: ${r}`);const s={};e.code!=null&&(s.code=e.code),e.sender_id!=null&&(s.sender_id=e.sender_id),e.policy!=null&&(s.policy=e.policy);try{const o=await t.agentInvoke("claude_access_control",{verb:n,payload:s},3e4);return this.successResult(o)}catch(o){const u=o instanceof Error?o.message:String(o);return this.errorResult(`access_control \u8C03\u7528\u5931\u8D25: ${u}`)}}}export{y as ToolExecutorImpl};
@@ -1 +1 @@
1
- import{TOOLS as o,EVENT_TOOLS as s}from"../../core/mcp/tools.js";const e=new Set(["grix_query","grix_group","grix_message_send","grix_message_unsend","grix_admin"]),r=new Set(["grix_reply","grix_complete","grix_event_ack","grix_composing","grix_access_control","grix_status"]);class a{tools;toolMap;constructor(){this.tools=[...o.filter(t=>e.has(t.name)),...s.filter(t=>r.has(t.name))],this.toolMap=new Map(this.tools.map(t=>[t.name,t]))}getTools(){return this.tools}getTool(t){return this.toolMap.get(t)}hasTool(t){return this.toolMap.has(t)}}export{a as ToolRegistryImpl};
1
+ import{TOOLS as s,EVENT_TOOLS as t}from"../../core/mcp/tools.js";const e=new Set(["grix_query","grix_group","grix_message_send","grix_message_unsend","grix_admin"]),r=new Set(["grix_reply","grix_complete","grix_event_ack","grix_composing","grix_access_control","grix_status"]);class a{tools;toolMap;constructor(){this.tools=[...s.filter(o=>e.has(o.name)),...t.filter(o=>r.has(o.name))],this.toolMap=new Map(this.tools.map(o=>[o.name,o]))}getTools(){return this.tools}getTool(o){return this.toolMap.get(o)}hasTool(o){return this.toolMap.has(o)}}export{a as ToolRegistryImpl};
@@ -1 +1 @@
1
- const o={required:["action"],properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search"]},id:{type:"string"},keyword:{type:"string",maxLength:200},limit:{type:"integer",minimum:1,maximum:100},offset:{type:"integer",minimum:0},sessionId:{type:"string"},beforeId:{type:"string"}}},a={required:["action"],properties:{action:{type:"string",enum:["create","detail","leave","add_members","remove_members","update_member_role","update_all_members_muted","update_member_speaking","dissolve"]},sessionId:{type:"string"},name:{type:"string",maxLength:128},memberIds:{type:"array",items:{type:"string"},maxItems:100},memberTypes:{type:"array",items:{type:"integer",enum:[1,2]}},memberId:{type:"string"},role:{type:"integer",enum:[1,2]},memberType:{type:"integer"},allMembersMuted:{type:"boolean"},isSpeakMuted:{type:"boolean"},canSpeakWhenAllMuted:{type:"boolean"}}},p={required:["sessionId","content"],properties:{sessionId:{type:"string"},content:{type:"string",maxLength:1e4},msgType:{type:"integer"},quotedMessageId:{type:"string"},threadId:{type:"string"}}},m={required:["sessionId","msgId"],properties:{sessionId:{type:"string"},msgId:{type:"string"}}},g={required:["action"],properties:{action:{type:"string",enum:["create_agent","list_categories","create_category","update_category","assign_category","rotate_api_key"]},agentName:{type:"string"},introduction:{type:"string"},isMain:{type:"boolean"},agentId:{type:"string"},categoryId:{type:"string"},name:{type:"string"},parentId:{type:"string"},sortOrder:{type:"integer"}}},y={required:["session_id","text"],properties:{event_id:{type:"string"},session_id:{type:"string"},text:{type:"string",maxLength:5e4},quoted_message_id:{type:"string"},is_final:{type:"boolean"}}},d={required:["event_id","status"],properties:{event_id:{type:"string"},status:{type:"string",enum:["responded","canceled","failed"]},msg:{type:"string",maxLength:500}}},c={required:["event_id"],properties:{event_id:{type:"string"},session_id:{type:"string"}}},l={required:["session_id","active"],properties:{session_id:{type:"string"},active:{type:"boolean"},event_id:{type:"string"}}},_={required:["action"],properties:{action:{type:"string",enum:["pair_approve","pair_deny","allow_sender","remove_sender","set_policy"]},code:{type:"string"},sender_id:{type:"string"},policy:{type:"string",enum:["allowlist","open","disabled"]}}},f={required:[],properties:{}},C={grix_query:o,grix_group:a,grix_message_send:p,grix_message_unsend:m,grix_admin:g,grix_reply:y,grix_complete:d,grix_event_ack:c,grix_composing:l,grix_access_control:_,grix_status:f};function B(r,t){const e=C[r];if(!e)return{valid:!1,error:`\u672A\u77E5\u5DE5\u5177: ${r}`};for(const i of e.required)if(t[i]===void 0||t[i]===null)return{valid:!1,error:`\u7F3A\u5C11\u5FC5\u586B\u53C2\u6570: ${i}`};for(const[i,u]of Object.entries(t)){if(u==null)continue;const n=e.properties[i];if(!n)continue;const s=$(i,u,n);if(s)return{valid:!1,error:s}}return{valid:!0}}function $(r,t,e){switch(e.type){case"string":if(typeof t!="string")return`\u53C2\u6570 ${r} \u7C7B\u578B\u9519\u8BEF: \u671F\u671B string\uFF0C\u5B9E\u9645 ${typeof t}`;if(e.maxLength!==void 0&&t.length>e.maxLength)return`\u53C2\u6570 ${r} \u8D85\u8FC7\u6700\u5927\u957F\u5EA6 ${e.maxLength}\uFF0C\u5B9E\u9645 ${t.length}`;if(e.enum&&!e.enum.includes(t))return`\u53C2\u6570 ${r} \u503C "${t}" \u4E0D\u5728\u5141\u8BB8\u8303\u56F4 [${e.enum.join(", ")}]`;break;case"integer":if(typeof t!="number"||!Number.isInteger(t))return`\u53C2\u6570 ${r} \u7C7B\u578B\u9519\u8BEF: \u671F\u671B integer\uFF0C\u5B9E\u9645 ${typeof t=="number"?"\u6D6E\u70B9\u6570":typeof t}`;if(e.minimum!==void 0&&t<e.minimum)return`\u53C2\u6570 ${r} \u503C ${t} \u5C0F\u4E8E\u6700\u5C0F\u503C ${e.minimum}`;if(e.maximum!==void 0&&t>e.maximum)return`\u53C2\u6570 ${r} \u503C ${t} \u5927\u4E8E\u6700\u5927\u503C ${e.maximum}`;if(e.enum&&!e.enum.includes(t))return`\u53C2\u6570 ${r} \u503C ${t} \u4E0D\u5728\u5141\u8BB8\u8303\u56F4 [${e.enum.join(", ")}]`;break;case"boolean":if(typeof t!="boolean")return`\u53C2\u6570 ${r} \u7C7B\u578B\u9519\u8BEF: \u671F\u671B boolean\uFF0C\u5B9E\u9645 ${typeof t}`;break;case"array":if(!Array.isArray(t))return`\u53C2\u6570 ${r} \u7C7B\u578B\u9519\u8BEF: \u671F\u671B array\uFF0C\u5B9E\u9645 ${typeof t}`;if(e.maxItems!==void 0&&t.length>e.maxItems)return`\u53C2\u6570 ${r} \u8D85\u8FC7\u6700\u5927\u5143\u7D20\u6570 ${e.maxItems}\uFF0C\u5B9E\u9645 ${t.length}`;if(e.items)for(let i=0;i<t.length;i++){const u=t[i];if(e.items.type==="string"&&typeof u!="string")return`\u53C2\u6570 ${r}[${i}] \u7C7B\u578B\u9519\u8BEF: \u671F\u671B string\uFF0C\u5B9E\u9645 ${typeof u}`;if(e.items.type==="integer"){if(typeof u!="number"||!Number.isInteger(u))return`\u53C2\u6570 ${r}[${i}] \u7C7B\u578B\u9519\u8BEF: \u671F\u671B integer\uFF0C\u5B9E\u9645 ${typeof u}`;if(e.items.enum&&!e.items.enum.includes(u))return`\u53C2\u6570 ${r}[${i}] \u503C ${u} \u4E0D\u5728\u5141\u8BB8\u8303\u56F4 [${e.items.enum.join(", ")}]`}}break}}export{B as validateToolArgs};
1
+ const o={required:["action"],properties:{action:{type:"string",enum:["contact_search","session_search","message_history","message_search"]},id:{type:"string"},keyword:{type:"string",maxLength:200},limit:{type:"integer",minimum:1,maximum:100},offset:{type:"integer",minimum:0},sessionId:{type:"string"},beforeId:{type:"string"}}},a={required:["action"],properties:{action:{type:"string",enum:["create","detail","leave","add_members","remove_members","update_member_role","update_all_members_muted","update_member_speaking","dissolve"]},sessionId:{type:"string"},name:{type:"string",maxLength:128},memberIds:{type:"array",items:{type:"string"},maxItems:100},memberTypes:{type:"array",items:{type:"integer",enum:[1,2]}},memberId:{type:"string"},role:{type:"integer",enum:[1,2]},memberType:{type:"integer"},allMembersMuted:{type:"boolean"},isSpeakMuted:{type:"boolean"},canSpeakWhenAllMuted:{type:"boolean"}}},p={required:["sessionId","content"],properties:{sessionId:{type:"string"},content:{type:"string",maxLength:1e4},msgType:{type:"integer"},quotedMessageId:{type:"string"},threadId:{type:"string"}}},m={required:["sessionId","msgId"],properties:{sessionId:{type:"string"},msgId:{type:"string"}}},g={required:["action"],properties:{action:{type:"string",enum:["create_agent","list_categories","create_category","update_category","assign_category","rotate_api_key"]},agentName:{type:"string"},introduction:{type:"string"},isMain:{type:"boolean"},agentId:{type:"string"},categoryId:{type:"string"},name:{type:"string"},parentId:{type:"string"},sortOrder:{type:"integer"}}},y={required:["session_id","text"],properties:{event_id:{type:"string"},session_id:{type:"string"},text:{type:"string",maxLength:5e4},quoted_message_id:{type:"string"},is_final:{type:"boolean"}}},d={required:["event_id","status"],properties:{event_id:{type:"string"},status:{type:"string",enum:["responded","canceled","failed"]},msg:{type:"string",maxLength:500}}},c={required:["event_id"],properties:{event_id:{type:"string"},session_id:{type:"string"}}},l={required:["session_id","active"],properties:{session_id:{type:"string"},active:{type:"boolean"},event_id:{type:"string"}}},_={required:["action"],properties:{action:{type:"string",enum:["pair_approve","pair_deny","allow_sender","remove_sender","set_policy"]},code:{type:"string"},sender_id:{type:"string"},policy:{type:"string",enum:["allowlist","open","disabled"]}}},f={required:[],properties:{}},C={grix_query:o,grix_group:a,grix_message_send:p,grix_message_unsend:m,grix_admin:g,grix_reply:y,grix_complete:d,grix_event_ack:c,grix_composing:l,grix_access_control:_,grix_status:f};function B(u,t){const e=C[u];if(!e)return{valid:!1,error:`\u672A\u77E5\u5DE5\u5177: ${u}`};for(const i of e.required)if(t[i]===void 0||t[i]===null)return{valid:!1,error:`\u7F3A\u5C11\u5FC5\u586B\u53C2\u6570: ${i}`};for(const[i,r]of Object.entries(t)){if(r==null)continue;const n=e.properties[i];if(!n)continue;const s=$(i,r,n);if(s)return{valid:!1,error:s}}return{valid:!0}}function $(u,t,e){switch(e.type){case"string":if(typeof t!="string")return`\u53C2\u6570 ${u} \u7C7B\u578B\u9519\u8BEF: \u671F\u671B string\uFF0C\u5B9E\u9645 ${typeof t}`;if(e.maxLength!==void 0&&t.length>e.maxLength)return`\u53C2\u6570 ${u} \u8D85\u8FC7\u6700\u5927\u957F\u5EA6 ${e.maxLength}\uFF0C\u5B9E\u9645 ${t.length}`;if(e.enum&&!e.enum.includes(t))return`\u53C2\u6570 ${u} \u503C "${t}" \u4E0D\u5728\u5141\u8BB8\u8303\u56F4 [${e.enum.join(", ")}]`;break;case"integer":if(typeof t!="number"||!Number.isInteger(t))return`\u53C2\u6570 ${u} \u7C7B\u578B\u9519\u8BEF: \u671F\u671B integer\uFF0C\u5B9E\u9645 ${typeof t=="number"?"\u6D6E\u70B9\u6570":typeof t}`;if(e.minimum!==void 0&&t<e.minimum)return`\u53C2\u6570 ${u} \u503C ${t} \u5C0F\u4E8E\u6700\u5C0F\u503C ${e.minimum}`;if(e.maximum!==void 0&&t>e.maximum)return`\u53C2\u6570 ${u} \u503C ${t} \u5927\u4E8E\u6700\u5927\u503C ${e.maximum}`;if(e.enum&&!e.enum.includes(t))return`\u53C2\u6570 ${u} \u503C ${t} \u4E0D\u5728\u5141\u8BB8\u8303\u56F4 [${e.enum.join(", ")}]`;break;case"boolean":if(typeof t!="boolean")return`\u53C2\u6570 ${u} \u7C7B\u578B\u9519\u8BEF: \u671F\u671B boolean\uFF0C\u5B9E\u9645 ${typeof t}`;break;case"array":if(!Array.isArray(t))return`\u53C2\u6570 ${u} \u7C7B\u578B\u9519\u8BEF: \u671F\u671B array\uFF0C\u5B9E\u9645 ${typeof t}`;if(e.maxItems!==void 0&&t.length>e.maxItems)return`\u53C2\u6570 ${u} \u8D85\u8FC7\u6700\u5927\u5143\u7D20\u6570 ${e.maxItems}\uFF0C\u5B9E\u9645 ${t.length}`;if(e.items)for(let i=0;i<t.length;i++){const r=t[i];if(e.items.type==="string"&&typeof r!="string")return`\u53C2\u6570 ${u}[${i}] \u7C7B\u578B\u9519\u8BEF: \u671F\u671B string\uFF0C\u5B9E\u9645 ${typeof r}`;if(e.items.type==="integer"){if(typeof r!="number"||!Number.isInteger(r))return`\u53C2\u6570 ${u}[${i}] \u7C7B\u578B\u9519\u8BEF: \u671F\u671B integer\uFF0C\u5B9E\u9645 ${typeof r}`;if(e.items.enum&&!e.items.enum.includes(r))return`\u53C2\u6570 ${u}[${i}] \u503C ${r} \u4E0D\u5728\u5141\u8BB8\u8303\u56F4 [${e.items.enum.join(", ")}]`}}break}}export{B as validateToolArgs};