@twinsearth/w2m-dsh-plugin 1.2.2 → 1.2.3
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +73 -27
- package/codex/mcp-servers.toml +1 -1
- package/lib/client.js +62 -3
- package/package.json +6 -4
- package/plugin.json +1 -1
- package/src/plugin/fleet-chat-route.mjs +46 -1
- package/src/plugin/schema-spec.mjs +49 -0
- package/src/plugin/tools.mjs +40 -4
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
7
|
"description": "The w2m fleet plugins: one account, many machines, any of Windows, macOS or Linux.",
|
|
8
|
-
"version": "1.2.
|
|
8
|
+
"version": "1.2.3"
|
|
9
9
|
},
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "w2m-fleet",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"description": "Read fleet state and dispatch commands across the machines registered to one account. Read-only MCP surface: the five exposed tools cannot change anything.",
|
|
15
|
-
"version": "1.2.
|
|
15
|
+
"version": "1.2.3",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "TwinsEarth"
|
|
18
18
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "w2m-fleet",
|
|
3
3
|
"description": "Drive the same project on online Windows, macOS and Linux machines from one account: read fleet state, dispatch commands and read results. The MCP surface is deliberately read-only.",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.3",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TwinsEarth"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,52 @@
|
|
|
7
7
|
|
|
8
8
|
All notable changes to this project are documented here.
|
|
9
9
|
|
|
10
|
+
## [1.2.3] — 2026-10-20
|
|
11
|
+
|
|
12
|
+
**The packed artifact did not mount. This fixes that, and the reason it was not caught here.**
|
|
13
|
+
|
|
14
|
+
`v1.2.2` was released, published to npm, and **failed Compat**: the packed artifact would not mount in a real profile.
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
JsonSchemaError: unsupported JSON schema: parameters.params.additionalProperties must be explicitly true or false
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`w2m_intent` declared `params: { type: "object", ... }` without stating `additionalProperties`. DSH refuses such a
|
|
21
|
+
schema **at mount time** -- in the profile of whoever installs the build -- and this repository had no check that would
|
|
22
|
+
have said so before shipping: the local loader gate mounted the artifact on a host whose compiler path differs, so it
|
|
23
|
+
stayed green while the real profile refused the entry.
|
|
24
|
+
|
|
25
|
+
### What changed
|
|
26
|
+
|
|
27
|
+
- `w2m_intent`'s `params` now states `additionalProperties: true`, and every tool schema is checked when it is defined.
|
|
28
|
+
- New `src/plugin/schema-spec.mjs`: a pure walker over schema-shaped values, and `schemaProblems()` which returns every
|
|
29
|
+
object node that does not state `additionalProperties` as a boolean -- all of them at once, not the first one.
|
|
30
|
+
- `defineTool` now runs that check, so a tool with an under-specified object parameter fails where it is written rather
|
|
31
|
+
than in someone else's profile.
|
|
32
|
+
- `test/schema-spec.test.mjs`: 5 tests on the walker and the checker (nested properties, array items, unions, multiple
|
|
33
|
+
offenders, `true` accepted as well as `false`).
|
|
34
|
+
|
|
35
|
+
### Panel polish
|
|
36
|
+
|
|
37
|
+
- **Thread grouping**: consecutive messages that share a thread are shown under one thread head. Only consecutive runs are
|
|
38
|
+
grouped, because re-sorting by thread would put an answer above the question it answers.
|
|
39
|
+
- **Unread count**: the trigger shows how many messages arrived since this conversation was last opened. The watermark is
|
|
40
|
+
local storage per conversation -- the relay stays stateless and no "who read what" is invented.
|
|
41
|
+
|
|
42
|
+
- **Task board**: the panel shows the relay's own task list (up to six rows: id prefix, mode, status) through a host
|
|
43
|
+
proxy (`/api/w2m/tasks`), so the browser never holds a device token.
|
|
44
|
+
|
|
45
|
+
Still missing from the panel: a timeout mark on an unanswered question, which needs the ask store from the turn state
|
|
46
|
+
machine.
|
|
47
|
+
### Verified
|
|
48
|
+
|
|
49
|
+
Gate GREEN twice, 15 of 15: 2555 tests / 2541 pass / 0 fail / 14 skip.
|
|
50
|
+
|
|
51
|
+
### Not verified
|
|
52
|
+
|
|
53
|
+
Compat itself: that workflow runs in its own environment, so this release is verified by the same local gate that missed
|
|
54
|
+
the defect. **Watch the Compat run for this tag.** If it fails again, the mount is failing for a different reason than the
|
|
55
|
+
schema and the log will say which entry and which fiber.
|
|
10
56
|
## [1.2.2] — 2026-10-20
|
|
11
57
|
|
|
12
58
|
**Fleet collaboration, part one: shared-memory writes, a council of machines, and the panel that reaches them.**
|
|
@@ -22,33 +68,33 @@ Creating the tag through the API had already triggered one run, which created th
|
|
|
22
68
|
collided with it. A release step that cannot be retried fails for the wrong reason, so `beta.yml` now refreshes an existing
|
|
23
69
|
release (notes + assets with `--clobber`) instead of refusing, and says so in the log.
|
|
24
70
|
|
|
25
|
-
### What is in it
|
|
26
|
-
|
|
27
|
-
- **Shared-memory writes**: `w2m_memory` gained `action: "append"` with `if_revision`. A stale revision comes back as a 409
|
|
28
|
-
the caller reads (revision, how far behind, which records it has not seen) and is deliberately **not** retried in the
|
|
29
|
-
transport -- retrying there turns a visible conflict into a silent overwrite.
|
|
30
|
-
- **A council of machines**: `src/plugin/council-fleet.mjs` keeps the single-machine decision rule and changes the
|
|
31
|
-
transport to chat messages on `council-<id>`, so every machine reads the same ballots and recounts the same outcome.
|
|
32
|
-
Weights are a pure function of the ballot; a tie is refused; one ballot per machine; a late ballot is refused by name.
|
|
33
|
-
`w2m_council` exposes `fleet-open` / `fleet-vote` / `fleet-decide` / `fleet-state` / `fleet-list`.
|
|
34
|
-
- **The panel**: a recipient picker (default "all"), a thread chip on messages that belong to a thread, click-to-reply with
|
|
35
|
-
a quoted line above the input, and the reply thread travelling with it. The host route refuses a single recipient that
|
|
36
|
-
is not in this fleet (`CHAT_UNKNOWN_RECIPIENT`) and says `CHAT_ROSTER_UNAVAILABLE` when the roster could not be read at
|
|
37
|
-
all -- a relay outage is not a typo in the recipient.
|
|
38
|
-
- **A readable view over memory**: `w2m_memory` read now returns `view` (counts by machine and by status, one line per
|
|
39
|
-
record). A row shape it does not recognise is shown as itself rather than dropped.
|
|
40
|
-
- **The beta pipeline is repeatable**: creating the tag had triggered a run that created the release, so the explicit
|
|
41
|
-
dispatch collided with it. The step now refreshes an existing release instead of refusing.
|
|
42
|
-
|
|
43
|
-
### Verified
|
|
44
|
-
|
|
45
|
-
Gate GREEN twice, 15 of 15: 2550 tests / 2536 pass / 0 fail / 14 skip. tools 125 (three append cases), council-fleet 12,
|
|
46
|
-
memory-view 6, client-bundle 25.
|
|
47
|
-
|
|
48
|
-
### Not verified
|
|
49
|
-
|
|
50
|
-
The fleet council and a directed message across two live machines, and the panel against a second machine. Those need two
|
|
51
|
-
machines on this build.
|
|
71
|
+
### What is in it
|
|
72
|
+
|
|
73
|
+
- **Shared-memory writes**: `w2m_memory` gained `action: "append"` with `if_revision`. A stale revision comes back as a 409
|
|
74
|
+
the caller reads (revision, how far behind, which records it has not seen) and is deliberately **not** retried in the
|
|
75
|
+
transport -- retrying there turns a visible conflict into a silent overwrite.
|
|
76
|
+
- **A council of machines**: `src/plugin/council-fleet.mjs` keeps the single-machine decision rule and changes the
|
|
77
|
+
transport to chat messages on `council-<id>`, so every machine reads the same ballots and recounts the same outcome.
|
|
78
|
+
Weights are a pure function of the ballot; a tie is refused; one ballot per machine; a late ballot is refused by name.
|
|
79
|
+
`w2m_council` exposes `fleet-open` / `fleet-vote` / `fleet-decide` / `fleet-state` / `fleet-list`.
|
|
80
|
+
- **The panel**: a recipient picker (default "all"), a thread chip on messages that belong to a thread, click-to-reply with
|
|
81
|
+
a quoted line above the input, and the reply thread travelling with it. The host route refuses a single recipient that
|
|
82
|
+
is not in this fleet (`CHAT_UNKNOWN_RECIPIENT`) and says `CHAT_ROSTER_UNAVAILABLE` when the roster could not be read at
|
|
83
|
+
all -- a relay outage is not a typo in the recipient.
|
|
84
|
+
- **A readable view over memory**: `w2m_memory` read now returns `view` (counts by machine and by status, one line per
|
|
85
|
+
record). A row shape it does not recognise is shown as itself rather than dropped.
|
|
86
|
+
- **The beta pipeline is repeatable**: creating the tag had triggered a run that created the release, so the explicit
|
|
87
|
+
dispatch collided with it. The step now refreshes an existing release instead of refusing.
|
|
88
|
+
|
|
89
|
+
### Verified
|
|
90
|
+
|
|
91
|
+
Gate GREEN twice, 15 of 15: 2550 tests / 2536 pass / 0 fail / 14 skip. tools 125 (three append cases), council-fleet 12,
|
|
92
|
+
memory-view 6, client-bundle 25.
|
|
93
|
+
|
|
94
|
+
### Not verified
|
|
95
|
+
|
|
96
|
+
The fleet council and a directed message across two live machines, and the panel against a second machine. Those need two
|
|
97
|
+
machines on this build.
|
|
52
98
|
|
|
53
99
|
## [1.2.1] — 2026-10-18
|
|
54
100
|
|
package/codex/mcp-servers.toml
CHANGED
package/lib/client.js
CHANGED
|
@@ -300,6 +300,12 @@ window.__ModuleLoader__.load({
|
|
|
300
300
|
'.w2mChat_replyCancel{margin-left:auto;border:none;background:none;color:var(--dsw-alias-label-secondary);cursor:pointer}',
|
|
301
301
|
'.w2mChat_msg{cursor:pointer}',
|
|
302
302
|
'.w2mChat_thread{margin-left:6px;opacity:.7}',
|
|
303
|
+
'.w2mChat_threadHead{font-size:11px;color:var(--dsw-alias-label-tertiary);padding:4px 10px 0}',
|
|
304
|
+
'.w2mChat_board{display:flex;flex-direction:column;gap:2px;padding:6px 10px;border-bottom:.5px solid var(--dsw-alias-border-default)}',
|
|
305
|
+
'.w2mChat_boardTitle{font-size:11px;color:var(--dsw-alias-label-tertiary)}',
|
|
306
|
+
'.w2mChat_boardRow{font-size:11px;color:var(--dsw-alias-label-secondary);font-family:ui-monospace,monospace}',
|
|
307
|
+
'.w2mChat_threadHead{font-size:11px;color:var(--dsw-alias-label-tertiary);padding:4px 10px 0}',
|
|
308
|
+
'.w2mChat_unseen{margin-left:6px;min-width:16px;padding:0 5px;border-radius:8px;background:var(--dsw-alias-accent-default);color:var(--dsw-alias-label-on-accent);font-size:10px;line-height:16px;text-align:center}',
|
|
303
309
|
'.w2mChat_text{white-space:pre-wrap;word-break:break-word}',
|
|
304
310
|
'.w2mChat_msg.is-session .w2mChat_text{color:var(--dsw-danger)}', '.w2mChat_msg.is-chat .w2mChat_text{color:var(--dsw-success)}', '.w2mChat_msg.is-remote .w2mChat_text{color:var(--dsw-text)}', '.w2mChat_avatar{display:inline-flex;width:16px;height:16px;margin-right:5px;vertical-align:-3px}',
|
|
305
311
|
'.w2mChat_compose{display:flex;gap:8px;padding:8px 10px;border-top:.5px solid var(--dsw-alias-border-l2);align-items:flex-end}',
|
|
@@ -621,6 +627,16 @@ window.__ModuleLoader__.load({
|
|
|
621
627
|
const [roster, setRoster] = React.useState(null);
|
|
622
628
|
const [toRecipient, setToRecipient] = React.useState('all');
|
|
623
629
|
const [replyTo, setReplyTo] = React.useState(null);
|
|
630
|
+
const [tasks, setTasks] = React.useState([]);
|
|
631
|
+
const [lastSeenMs, setLastSeenMs] = React.useState(() => {
|
|
632
|
+
try {
|
|
633
|
+
const raw = globalThis.localStorage?.getItem('w2m:chat:seen:' + sessionId);
|
|
634
|
+
const parsed = Number.parseInt(raw ?? '', 10);
|
|
635
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
636
|
+
} catch {
|
|
637
|
+
return 0;
|
|
638
|
+
}
|
|
639
|
+
});
|
|
624
640
|
const [text, setText] = React.useState('');
|
|
625
641
|
const [error, setError] = React.useState(null);
|
|
626
642
|
const [busy, setBusy] = React.useState(false);
|
|
@@ -666,6 +682,40 @@ window.__ModuleLoader__.load({
|
|
|
666
682
|
});
|
|
667
683
|
return () => controller.abort();
|
|
668
684
|
}, [open, sessionId]);
|
|
685
|
+
|
|
686
|
+
// The task board is the relay's own list: the host proxies it so the panel never holds a device token.
|
|
687
|
+
React.useEffect(() => {
|
|
688
|
+
if (!open) return undefined;
|
|
689
|
+
const controller = new AbortController();
|
|
690
|
+
chatCall({ url: '/api/w2m/tasks?limit=20', init: { signal: controller.signal } })
|
|
691
|
+
.then((result) => { if (!controller.signal.aborted) setTasks(result.ok && Array.isArray(result.data?.tasks) ? result.data.tasks : []); })
|
|
692
|
+
.catch(() => { /* the board is an extra; a failed read must not break the chat */ });
|
|
693
|
+
return () => controller.abort();
|
|
694
|
+
}, [open]);
|
|
695
|
+
|
|
696
|
+
// Unread is measured against a per-conversation watermark in local storage: the relay stays stateless, and
|
|
697
|
+
// the count answers "is there anything I have not looked at" without claiming which messages a machine read.
|
|
698
|
+
React.useEffect(() => {
|
|
699
|
+
if (!open || messages.length === 0) return undefined;
|
|
700
|
+
const latest = messages.reduce((max, message) => (Number.isFinite(message?.at_ms) ? Math.max(max, message.at_ms) : max), 0);
|
|
701
|
+
if (latest <= 0 || latest <= lastSeenMs) return undefined;
|
|
702
|
+
setLastSeenMs(latest);
|
|
703
|
+
try { globalThis.localStorage?.setItem('w2m:chat:seen:' + sessionId, String(latest)); } catch { /* storage may be unavailable */ }
|
|
704
|
+
return undefined;
|
|
705
|
+
}, [open, messages, sessionId, lastSeenMs]);
|
|
706
|
+
|
|
707
|
+
// Consecutive messages that share a thread are shown under one thread head. Grouping only consecutive runs keeps
|
|
708
|
+
// the reading order the relay assigned -- re-sorting by thread would put an answer above the question it answers.
|
|
709
|
+
const groupThreads = (list) => {
|
|
710
|
+
const groups = [];
|
|
711
|
+
for (const message of list ?? []) {
|
|
712
|
+
const threadId = typeof message?.thread_id === 'string' && message.thread_id !== '' ? message.thread_id : null;
|
|
713
|
+
const last = groups[groups.length - 1] ?? null;
|
|
714
|
+
if (last !== null && last.threadId === threadId) { last.messages.push(message); continue; }
|
|
715
|
+
groups.push({ threadId, messages: [message] });
|
|
716
|
+
}
|
|
717
|
+
return groups;
|
|
718
|
+
};
|
|
669
719
|
|
|
670
720
|
const send = React.useCallback(async () => {
|
|
671
721
|
const value = text.trim();
|
|
@@ -696,6 +746,7 @@ window.__ModuleLoader__.load({
|
|
|
696
746
|
return undefined;
|
|
697
747
|
}, [busy, load, sessionId, text]);
|
|
698
748
|
|
|
749
|
+
const unseenCount = messages.reduce((count, message) => (Number.isFinite(message?.at_ms) && message.at_ms > lastSeenMs ? count + 1 : count), 0);
|
|
699
750
|
const disabled = typeof sessionId !== 'string' || sessionId === '';
|
|
700
751
|
const usageText = usage === null ? null : `${Math.round((usage.used_24h_bytes ?? 0) / 1024)} KiB/24h · ${usage.messages ?? 0} 条`;
|
|
701
752
|
const machineLabel = (message) => {
|
|
@@ -722,7 +773,7 @@ window.__ModuleLoader__.load({
|
|
|
722
773
|
'aria-expanded': open,
|
|
723
774
|
disabled,
|
|
724
775
|
onClick: () => setOpen((value) => !value),
|
|
725
|
-
}, '💬', h('span', { className: 'w2mChat_label' }, '编队聊天')),
|
|
776
|
+
}, '💬', h('span', { className: 'w2mChat_label' }, '编队聊天'), unseenCount > 0 && !open ? h('span', { className: 'w2mChat_unseen' }, String(unseenCount)) : null),
|
|
726
777
|
open
|
|
727
778
|
? h('div', { className: 'w2mChat_panel', role: 'dialog', 'aria-label': '编队聊天' },
|
|
728
779
|
h('div', { className: 'w2mChat_head' },
|
|
@@ -738,16 +789,24 @@ window.__ModuleLoader__.load({
|
|
|
738
789
|
h('span', null, '编队聊天'),
|
|
739
790
|
usageText === null ? null : h('span', { className: 'w2mChat_usage' }, usageText),
|
|
740
791
|
h('button', { type: 'button', className: 'w2mChat_close', title: '关闭', onClick: () => setOpen(false) }, '×')),
|
|
792
|
+
tasks.length === 0 ? null : h('div', { className: 'w2mChat_board' }, [
|
|
793
|
+
h('div', { key: 'title', className: 'w2mChat_boardTitle' }, '任务看板'),
|
|
794
|
+
...tasks.slice(0, 6).map((task) => h('div', { key: String(task?.task_id ?? Math.random()), className: 'w2mChat_boardRow' },
|
|
795
|
+
`${String(task?.task_id ?? '').slice(0, 8)} ${task?.mode ?? '?'} ${task?.status ?? '?'}`)),
|
|
796
|
+
]),
|
|
741
797
|
h('div', { className: 'w2mChat_list' },
|
|
742
798
|
error !== null ? h('div', { className: 'w2mChat_error' }, error) : null,
|
|
743
799
|
messages.length === 0
|
|
744
800
|
? (error === null ? h('div', { className: 'w2mChat_empty' }, '本会话还没有消息') : null)
|
|
745
|
-
: messages.
|
|
801
|
+
: groupThreads(messages).flatMap((group, groupIndex) => [
|
|
802
|
+
group.threadId === null ? null : h('div', { key: 'thread:' + group.threadId + ':' + groupIndex, className: 'w2mChat_threadHead' }, '#' + group.threadId),
|
|
803
|
+
...group.messages.map((message) => {
|
|
746
804
|
var kind = messageKind(message, localMachineId);
|
|
747
805
|
return h('div', { key: message?.message_id ?? `${message?.machine_id}:${message?.at_ms}`, className: 'w2mChat_msg is-' + kind, onClick: () => setReplyTo({ id: message?.message_id ?? null, threadId: message?.thread_id ?? null, text: typeof message?.text === 'string' ? message.text : '' }) },
|
|
748
806
|
h('div', { className: 'w2mChat_meta' }, h(WhaleAvatar, { machineId: message?.machine_id }), `${machineLabel(message)} · ${timeLabel(message)}`, kind === 'session' ? ' · 来自会话' : '', message?.thread_id ? h('span', { className: 'w2mChat_thread' }, '#' + message.thread_id) : null),
|
|
749
807
|
h('div', { className: 'w2mChat_text' }, message?.text ?? ''));
|
|
750
|
-
|
|
808
|
+
}),
|
|
809
|
+
].filter((node) => node !== null))),
|
|
751
810
|
|
|
752
811
|
|
|
753
812
|
h('div', { className: 'w2mChat_compose' },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 11 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -64,14 +64,14 @@
|
|
|
64
64
|
"pack": "node scripts/pack.mjs",
|
|
65
65
|
"reachability": "node scripts/reachability-check.mjs",
|
|
66
66
|
"reachability:classify": "node scripts/reachability-classify.mjs",
|
|
67
|
-
"test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs test/update-channels.test.mjs test/update-prerelease.test.mjs test/session-watch.test.mjs test/session-readonly.test.mjs test/session-sync.test.mjs test/fleet-chat-identity.test.mjs test/identity-reader.test.mjs test/intents.test.mjs test/dataflow.test.mjs test/council-fleet.test.mjs test/fleet-memory.test.mjs test/memory-view.test.mjs",
|
|
67
|
+
"test": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-crypto.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs test/update-channels.test.mjs test/update-prerelease.test.mjs test/session-watch.test.mjs test/session-readonly.test.mjs test/session-sync.test.mjs test/fleet-chat-identity.test.mjs test/identity-reader.test.mjs test/intents.test.mjs test/dataflow.test.mjs test/council-fleet.test.mjs test/fleet-memory.test.mjs test/memory-view.test.mjs test/schema-spec.test.mjs test/fleet-weight-parity.test.mjs",
|
|
68
68
|
"test:acceptance-doc": "node --test --test-force-exit test/acceptance-doc.test.mjs",
|
|
69
69
|
"test:account-identity": "node --test --test-force-exit test/account-identity.test.mjs",
|
|
70
70
|
"test:account-join": "node --test --test-force-exit test/account-join.test.mjs",
|
|
71
71
|
"test:admission": "node --test --test-force-exit test/admission.test.mjs",
|
|
72
72
|
"test:admission-routes": "node --test --test-force-exit test/admission-routes.test.mjs",
|
|
73
73
|
"test:agent": "node --test --test-force-exit test/agent.test.mjs test/memory.test.mjs",
|
|
74
|
-
"test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/p2p-crypto.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs test/update-channels.test.mjs test/update-prerelease.test.mjs test/session-watch.test.mjs test/session-readonly.test.mjs test/session-sync.test.mjs test/fleet-chat-identity.test.mjs test/identity-reader.test.mjs test/intents.test.mjs test/dataflow.test.mjs test/council-fleet.test.mjs test/fleet-memory.test.mjs test/memory-view.test.mjs",
|
|
74
|
+
"test:all": "node --test --test-force-exit test/relay.test.mjs test/broadcast.test.mjs test/blake2s.test.mjs test/agent.test.mjs test/plan-sec-cmd.test.mjs test/authority.test.mjs test/blocks.test.mjs test/approval-index.test.mjs test/fleet-sync.test.mjs test/fleet-merge.test.mjs test/fleet-memory-e2e.test.mjs test/fleet-concurrency.test.mjs test/fleet-merge-race.test.mjs test/health.test.mjs test/storage.test.mjs test/exit-channel.test.mjs test/council.test.mjs test/council-session.test.mjs test/log-bounds.test.mjs test/test-report.test.mjs test/fleet-scale.test.mjs test/fleet-state.test.mjs test/crypto-blake2s.test.mjs test/installed-version.test.mjs test/permission.test.mjs test/machine-test-packet.test.mjs test/field-authority.test.mjs test/approval-gate.test.mjs test/authority-choice.test.mjs test/reachability-check.test.mjs test/reachability-classify.test.mjs test/accepted-orphans.test.mjs test/acceptance-doc.test.mjs test/layer-model.test.mjs test/cwd-containment.test.mjs test/cwd-outside-project.test.mjs test/memory.test.mjs test/tools.test.mjs test/claims.test.mjs test/fleet-target.test.mjs test/client-bundle.test.mjs test/schedule.test.mjs test/auto-update.test.mjs test/update-source.test.mjs test/update-install.test.mjs test/update-wiring.test.mjs test/signing.test.mjs test/stress.test.mjs test/e2e.test.mjs test/crossnetwork.test.mjs test/signing-e2e.test.mjs test/outbound-only.test.mjs test/recovery.test.mjs test/pipeline-e2e.test.mjs test/shared-config.test.mjs test/metrics.test.mjs test/metrics-route.test.mjs test/tool-cards.test.mjs test/p2p-stun.test.mjs test/p2p-transport.test.mjs test/p2p-signaling.test.mjs test/p2p-node.test.mjs test/p2p-transport-fields.test.mjs test/p2p-plugin.test.mjs test/p2p-agent.test.mjs test/stun-server.test.mjs test/loader-entry.test.mjs test/mdns-wire.test.mjs test/mdns-discover.test.mjs test/p2p-crypto.test.mjs test/chatops-approval.test.mjs test/chatops-policy.test.mjs test/chatops-file-safety.test.mjs test/chatops-outbound-queue.test.mjs test/chatops-commands.test.mjs test/chatops-bridge.test.mjs test/chatops-config-route.test.mjs test/chatops-apply.test.mjs test/chatops-ilink-contract.test.mjs test/chatops-fleet.test.mjs test/chatops-account.test.mjs test/chatops-auth.test.mjs test/chatops-fleet-scope.test.mjs test/chatops-handoff.test.mjs test/chatops-fleet-wiring.test.mjs test/chatops-fleet-live.test.mjs test/chatops-takeover.test.mjs test/chatops-device-signing.test.mjs test/chatops-relay-verify.test.mjs test/chatops-durability.test.mjs test/chatops-run-command.test.mjs test/secrets-guard.test.mjs test/chatops-ilink-fleet-bridge.test.mjs test/agent-plugins.test.mjs test/memory-layer.test.mjs test/mcp-server.test.mjs test/portable-plugins.test.mjs test/mcp-http.test.mjs test/mcp-http-live.test.mjs test/vision.test.mjs test/ilink-shape.test.mjs test/chatops-fleet-join.test.mjs test/admission.test.mjs test/admission-routes.test.mjs test/join-degradation.test.mjs test/account-identity.test.mjs test/join-challenge.test.mjs test/exec-env-injection.test.mjs test/account-join.test.mjs test/fleet-messages.test.mjs test/message-shards.test.mjs test/fleet-messages-http.test.mjs test/message-notify.test.mjs test/update-channels.test.mjs test/update-prerelease.test.mjs test/session-watch.test.mjs test/session-readonly.test.mjs test/session-sync.test.mjs test/fleet-chat-identity.test.mjs test/identity-reader.test.mjs test/intents.test.mjs test/dataflow.test.mjs test/council-fleet.test.mjs test/fleet-memory.test.mjs test/memory-view.test.mjs test/schema-spec.test.mjs test/fleet-weight-parity.test.mjs",
|
|
75
75
|
"test:approval-gate": "node --test --test-force-exit test/approval-gate.test.mjs",
|
|
76
76
|
"test:approval-index": "node --test --test-force-exit test/approval-index.test.mjs",
|
|
77
77
|
"test:authority": "node --test --test-force-exit test/authority.test.mjs",
|
|
@@ -126,7 +126,9 @@
|
|
|
126
126
|
"test:dataflow": "node --test --test-force-exit test/dataflow.test.mjs",
|
|
127
127
|
"test:council-fleet": "node --test --test-force-exit test/council-fleet.test.mjs",
|
|
128
128
|
"test:fleet-memory": "node --test --test-force-exit test/fleet-memory.test.mjs",
|
|
129
|
-
"test:memory-view": "node --test --test-force-exit test/memory-view.test.mjs"
|
|
129
|
+
"test:memory-view": "node --test --test-force-exit test/memory-view.test.mjs",
|
|
130
|
+
"test:schema-spec": "node --test --test-force-exit test/schema-spec.test.mjs",
|
|
131
|
+
"test:fleet-weight-parity": "node --test --test-force-exit test/fleet-weight-parity.test.mjs"
|
|
130
132
|
},
|
|
131
133
|
"devDependencies": {
|
|
132
134
|
"@eslint/js": "^10.0.1",
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "twinsearth-w2m-dsh",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.3",
|
|
5
5
|
"description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 12 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
|
@@ -42,7 +42,10 @@ function newestTranscript(cwd) {
|
|
|
42
42
|
return best === null ? null : best.file;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export const CHAT_ROUTE_PATH = '/api/w2m/chat';
|
|
45
|
+
export const CHAT_ROUTE_PATH = '/api/w2m/chat';
|
|
46
|
+
|
|
47
|
+
/** The light task board the panel shows beside the chat. The relay already answers GET /v1/tasks. */
|
|
48
|
+
export const TASKS_ROUTE_PATH = '/api/w2m/tasks';
|
|
46
49
|
export const MAX_SESSION_ID_LENGTH = 200;
|
|
47
50
|
|
|
48
51
|
const NO_STORE = { 'cache-control': 'no-store' };
|
|
@@ -284,6 +287,48 @@ export function registerChatRoutes(ctx, deps) {
|
|
|
284
287
|
}
|
|
285
288
|
},
|
|
286
289
|
});
|
|
290
|
+
// A light task board. The relay answers GET /v1/tasks; this proxy exists so the panel can read it with the device
|
|
291
|
+
// token that never leaves the host.
|
|
292
|
+
registry.register({
|
|
293
|
+
path: TASKS_ROUTE_PATH,
|
|
294
|
+
methods: ['GET'],
|
|
295
|
+
requestBody: 'buffered',
|
|
296
|
+
fetch: async (request) => {
|
|
297
|
+
const refuse = (httpStatus, reasonCode, error, detail) =>
|
|
298
|
+
Response.json({ ok: false, error, reason_code: reasonCode, detail }, { status: httpStatus, headers: NO_STORE });
|
|
299
|
+
if (request.method !== 'GET') {
|
|
300
|
+
return refuse(405, 'TASKS_METHOD_NOT_ALLOWED', `${request.method} is not answered here`, 'GET only');
|
|
301
|
+
}
|
|
302
|
+
const url = new URL(request.url);
|
|
303
|
+
const limitRaw = url.searchParams.get('limit');
|
|
304
|
+
const limit = limitRaw !== null && /^[0-9]+$/.test(limitRaw) ? Math.min(Number(limitRaw), 50) : 20;
|
|
305
|
+
const cfg = deps.cfg();
|
|
306
|
+
const rabbitUrl = cfg?.rabbitUrl ?? null;
|
|
307
|
+
if (rabbitUrl === null) return refuse(503, 'TASKS_NO_RELAY', 'no relay is configured', 'set rabbitUrl in the profile patch');
|
|
308
|
+
let token;
|
|
309
|
+
try {
|
|
310
|
+
token = await deps.resolveToken();
|
|
311
|
+
} catch (error) {
|
|
312
|
+
return refuse(503, 'TASKS_UNPAIRED', `this machine has no device token: ${error instanceof Error ? error.message : String(error)}`, 'pair the machine first');
|
|
313
|
+
}
|
|
314
|
+
if (typeof token !== 'string' || token === '') {
|
|
315
|
+
return refuse(503, 'TASKS_UNPAIRED', 'this machine has no device token', 'pair the machine first');
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
const listed = await deps.request({
|
|
319
|
+
rabbitUrl, token, method: 'GET', pathname: '/v1/tasks', query: { limit: String(limit) }, signal: request.signal,
|
|
320
|
+
});
|
|
321
|
+
if (listed?.ok !== true) {
|
|
322
|
+
const code = listed?.json?.error?.code ?? 'TASKS_LIST_REFUSED';
|
|
323
|
+
return refuse(502, code, `the relay answered HTTP ${listed?.status ?? 0}`, 'the relay must run a version with /v1/tasks');
|
|
324
|
+
}
|
|
325
|
+
return Response.json({ ...listed.json, ok: true }, { headers: NO_STORE });
|
|
326
|
+
} catch (error) {
|
|
327
|
+
return refuse(502, 'TASKS_RELAY_ERROR', `the relay call failed: ${error instanceof Error ? error.message : String(error)}`, null);
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
|
|
287
332
|
status.mounted = true;
|
|
288
333
|
status.reason = null;
|
|
289
334
|
const timer = setInterval(() => { mirrorOnce().catch(() => { /* one bad tick must not stop the next */ }); }, 2000);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authoring rules DSH enforces on tool schemas, checked here so the failure happens at build time in this repository
|
|
3
|
+
* rather than at mount time in someone else's profile.
|
|
4
|
+
*
|
|
5
|
+
* The rule this exists for: DSH's `parameterSchemaSpecToJsonSchema` requires an EXPLICIT `additionalProperties` on every
|
|
6
|
+
* `type: 'object'` node and throws otherwise. Local tests passed while the packed artifact failed in a real profile,
|
|
7
|
+
* which is exactly the gap a pure checker closes: the same walk runs in the suite and at registration.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Every `type: 'object'` node in a JSON-schema-shaped value, with its path.
|
|
12
|
+
*
|
|
13
|
+
* @param {unknown} schema
|
|
14
|
+
* @param {string} [path]
|
|
15
|
+
* @returns {Array<{path: string, node: object}>}
|
|
16
|
+
*/
|
|
17
|
+
export function objectNodes(schema, path = 'schema') {
|
|
18
|
+
const found = [];
|
|
19
|
+
if (schema === null || typeof schema !== 'object') return found;
|
|
20
|
+
const node = schema;
|
|
21
|
+
if (node.type === 'object') found.push({ path, node });
|
|
22
|
+
if (node.properties !== null && typeof node.properties === 'object') {
|
|
23
|
+
for (const [name, child] of Object.entries(node.properties)) {
|
|
24
|
+
found.push(...objectNodes(child, path + '.properties.' + name));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (node.items !== undefined) found.push(...objectNodes(node.items, path + '[]'));
|
|
28
|
+
for (const key of ['oneOf', 'anyOf', 'allOf']) {
|
|
29
|
+
if (Array.isArray(node[key])) {
|
|
30
|
+
node[key].forEach((child, index) => found.push(...objectNodes(child, path + '.' + key + '[' + index + ']')));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return found;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The problems a schema would be refused for, as `{path, why}` values rather than a thrown error, so a caller can report
|
|
38
|
+
* all of them at once instead of fixing one per run.
|
|
39
|
+
*
|
|
40
|
+
* @param {unknown} schema
|
|
41
|
+
* @param {string} [name] tool name, used in the path
|
|
42
|
+
* @returns {Array<{path: string, why: string}>}
|
|
43
|
+
*/
|
|
44
|
+
export function schemaProblems(schema, name = 'tool') {
|
|
45
|
+
return objectNodes(schema, 'parameters')
|
|
46
|
+
.filter(({ node }) => typeof node.additionalProperties !== 'boolean')
|
|
47
|
+
.map(({ path }) => ({ path: path + '.additionalProperties', why: 'an object parameter must state additionalProperties: true or false' }))
|
|
48
|
+
.map((problem) => ({ ...problem, path: name + '.' + problem.path }));
|
|
49
|
+
}
|
package/src/plugin/tools.mjs
CHANGED
|
@@ -185,6 +185,7 @@ import {
|
|
|
185
185
|
import { registerChatRoutes } from './fleet-chat-route.mjs';
|
|
186
186
|
import { createFleetCouncil, fleetCouncilThread } from './council-fleet.mjs';
|
|
187
187
|
import { renderMemoryView } from './memory-view.mjs';
|
|
188
|
+
import { schemaProblems } from './schema-spec.mjs';
|
|
188
189
|
import { listIntents, resolveIntent } from './intents.mjs';
|
|
189
190
|
// v0.7.0: the LAN hint layer. Imported here and used in exactly one place (`w2m_devices`); see the
|
|
190
191
|
// module header for why a discovered device is a candidate to *ask* and never an authorisation.
|
|
@@ -223,7 +224,7 @@ const MAX_STAGES = 16;
|
|
|
223
224
|
* `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
|
|
224
225
|
* overrides it at runtime for tests and packagers.
|
|
225
226
|
*/
|
|
226
|
-
const PLUGIN_VERSION = '1.2.
|
|
227
|
+
const PLUGIN_VERSION = '1.2.3';
|
|
227
228
|
|
|
228
229
|
/** How long any single HTTP call may take before it is aborted, in milliseconds. */
|
|
229
230
|
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
|
@@ -556,6 +557,15 @@ function createDefineTool(real) {
|
|
|
556
557
|
parameters[key] = normaliseParameter(name, key, decl);
|
|
557
558
|
}
|
|
558
559
|
|
|
560
|
+
// v1.2.3: DSH refuses an object parameter that does not state additionalProperties explicitly, and it
|
|
561
|
+
// refuses it at MOUNT time -- in the profile of whoever installed the build. That is the failure the packed
|
|
562
|
+
// artifact hit in a real host while this repository tests were green, so the rule is enforced here, where
|
|
563
|
+
// every tool already passes through.
|
|
564
|
+
const schemaProblemsFound = schemaProblems({ type: 'object', properties: parameters, additionalProperties: false }, name);
|
|
565
|
+
if (schemaProblemsFound.length > 0) {
|
|
566
|
+
throw new TypeError(schemaProblemsFound.map((problem) => problem.path + ': ' + problem.why).join('; '));
|
|
567
|
+
}
|
|
568
|
+
|
|
559
569
|
return { ...definition, parameters };
|
|
560
570
|
};
|
|
561
571
|
|
|
@@ -6173,7 +6183,7 @@ export async function apply(ctx, config = {}) {
|
|
|
6173
6183
|
skipped_lines: current.skipped,
|
|
6174
6184
|
truncated: current.truncated,
|
|
6175
6185
|
status_tally: tally(current.records),
|
|
6176
|
-
records: current.records,
|
|
6186
|
+
records: current.records,
|
|
6177
6187
|
view: renderMemoryView(current.records),
|
|
6178
6188
|
...(previous === null
|
|
6179
6189
|
? {}
|
|
@@ -6508,7 +6518,11 @@ export async function apply(ctx, config = {}) {
|
|
|
6508
6518
|
'command the machine allow-list does not carry is refused with the list it checked.',
|
|
6509
6519
|
parameters: {
|
|
6510
6520
|
action: { type: 'string', description: 'The catalog action; call with action:"list" to read the catalog.' },
|
|
6511
|
-
params: {
|
|
6521
|
+
params: {
|
|
6522
|
+
type: 'object',
|
|
6523
|
+
additionalProperties: true,
|
|
6524
|
+
description: 'Action parameters, for example { n: 3 } for shard-probe.',
|
|
6525
|
+
},
|
|
6512
6526
|
},
|
|
6513
6527
|
output: jsonOutput,
|
|
6514
6528
|
presentCall: (args) => ({ card: 'generic', title: 'w2m intent ' + (typeof args?.action === 'string' ? args.action : '?'), kind: 'read', rawInput: {} }),
|
|
@@ -6609,7 +6623,29 @@ export async function apply(ctx, config = {}) {
|
|
|
6609
6623
|
}
|
|
6610
6624
|
if (action === 'fleet-decide') {
|
|
6611
6625
|
const decided = fleetCouncil.decide({ councilId: id });
|
|
6612
|
-
|
|
6626
|
+
// The decision travels as an append-only memory record: a decision that exists only in one process memory
|
|
6627
|
+
// is a decision nobody can audit later. Writing it is best-effort -- the outcome is returned either way and
|
|
6628
|
+
// `trail` says which happened, rather than implying the record exists when it does not.
|
|
6629
|
+
let trail = 'not-written';
|
|
6630
|
+
if (decided.ok === true && decided.council?.decision != null) {
|
|
6631
|
+
try {
|
|
6632
|
+
const trailUrl = cfg?.rabbitUrl ?? null;
|
|
6633
|
+
const trailToken = trailUrl === null ? null : await resolveToken(cfg);
|
|
6634
|
+
if (trailUrl !== null && typeof trailToken === 'string' && trailToken !== '') {
|
|
6635
|
+
const decision = decided.council.decision;
|
|
6636
|
+
const line = 'council ' + String(id) + ': ' + String(decision.choice) + ' (weight ' + String(decision.weight) + ', '
|
|
6637
|
+
+ String(decided.council.ballots.length) + ' ballot(s))';
|
|
6638
|
+
const appended = await request({
|
|
6639
|
+
rabbitUrl: trailUrl, token: trailToken, method: 'PUT',
|
|
6640
|
+
pathname: '/v1/fleet/memory/fleet', body: { records: [line] }, signal: exec?.signal,
|
|
6641
|
+
});
|
|
6642
|
+
trail = appended?.ok === true ? 'appended' : 'refused';
|
|
6643
|
+
}
|
|
6644
|
+
} catch {
|
|
6645
|
+
trail = 'error';
|
|
6646
|
+
}
|
|
6647
|
+
}
|
|
6648
|
+
return JSON.stringify({ ok: decided.ok, action, council_id: id, thread_id: threadId, reason_code: decided.code ?? null, detail: decided.detail ?? null, trail, council: decided.council ?? null });
|
|
6613
6649
|
}
|
|
6614
6650
|
if (action === 'fleet-list') {
|
|
6615
6651
|
return JSON.stringify({ ok: true, action, ...fleetCouncil.list() });
|