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.
- package/dist/adapter/claude/claude-adapter.js +18 -17
- package/dist/adapter/claude/claude-bridge-server.js +1 -1
- package/dist/adapter/claude/claude-tools.js +1 -1
- package/dist/adapter/claude/claude-worker-client.js +1 -1
- package/dist/adapter/claude/mcp-http-launcher.js +2 -2
- package/dist/adapter/claude/result-timeout.js +1 -1
- package/dist/assets/dsh-bridge/{grix-dsh-bridge-4.2.3.tgz → grix-dsh-bridge-4.2.4.tgz} +0 -0
- package/dist/assets/dsh-bridge/manifest.json +4 -4
- package/dist/core/access/allowlist-store.js +1 -1
- package/dist/core/file-ops/list-files.js +1 -1
- package/dist/default-skills/grix-access-control/SKILL.md +1 -1
- package/dist/default-skills/grix-admin/SKILL.md +110 -155
- package/dist/default-skills/grix-admin/references/api-contract.md +23 -12
- package/dist/default-skills/grix-agent-dispatch/SKILL.md +152 -188
- package/dist/default-skills/grix-audit-data/SKILL.md +1 -1
- package/dist/default-skills/grix-chat-state/SKILL.md +26 -43
- package/dist/default-skills/grix-egg/SKILL.md +1 -1
- package/dist/default-skills/grix-group/SKILL.md +1 -1
- package/dist/default-skills/grix-owner-relay/SKILL.md +30 -64
- package/dist/default-skills/grix-query/SKILL.md +1 -1
- package/dist/default-skills/grix-skill-library/SKILL.md +52 -0
- package/dist/default-skills/grix-widget-visitor-ban/SKILL.md +1 -1
- package/dist/default-skills/message-send/SKILL.md +1 -1
- package/dist/default-skills/message-unsend/SKILL.md +1 -1
- package/dist/default-skills/tailnet-file-share/SKILL.md +38 -72
- package/dist/default-skills/tailnet-file-share/references/certificate-trust.md +64 -0
- package/dist/log.js +2 -2
- package/dist/mcp/stream-http/config.js +1 -1
- package/dist/mcp/stream-http/connection-binding.js +1 -1
- package/dist/mcp/stream-http/security.js +1 -1
- package/dist/mcp/stream-http/tool-executor.js +1 -1
- package/dist/mcp/stream-http/tool-registry.js +1 -1
- package/dist/mcp/stream-http/tool-schemas.js +1 -1
- package/openclaw-plugin/skills/grix-admin/SKILL.md +122 -156
- package/openclaw-plugin/skills/grix-group/SKILL.md +52 -159
- package/openclaw-plugin/skills/grix-query/SKILL.md +54 -248
- package/openclaw-plugin/skills/grix-update/SKILL.md +70 -304
- package/openclaw-plugin/skills/message-send/SKILL.md +41 -172
- package/openclaw-plugin/skills/message-unsend/SKILL.md +29 -170
- 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:
|
|
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
|
-
|
|
9
|
+
Two tools that involve the owner directly.
|
|
10
10
|
|
|
11
11
|
## Speak as the owner — `grix_session_send`
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
18
|
-
- `
|
|
19
|
-
|
|
20
|
-
quote
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
`grix-agent-dispatch
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
69
|
-
to discuss something or get an approval/review during your work.
|
|
70
|
-
owner an offline notification; tapping it lands them in the
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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:
|
|
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:
|
|
4
|
-
trigger:
|
|
3
|
+
description: Deliver a local file to the user — as 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
|
|
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.
|
|
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 ``, 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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
##
|
|
28
|
+
## 2. Call it and relay the result
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
`
|
|
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 `` and
|
|
37
|
+
render as an inline preview;
|
|
38
|
+
- everything else comes back as `[filename](url)` for download.
|
|
34
39
|
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
2
|
-
`)},error(o,r,...
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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};
|