@twinsearth/w2m-dsh-plugin 1.1.15 → 1.1.16

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.
@@ -12,6 +12,6 @@
12
12
 
13
13
  [mcp_servers.w2m-fleet]
14
14
  command = "npx"
15
- args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@1.1.15", "w2m-mcp"]
15
+ args = ["-y", "-p", "@twinsearth/w2m-dsh-plugin@1.1.16", "w2m-mcp"]
16
16
  startup_timeout_sec = 20
17
17
  tool_timeout_sec = 60
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinsearth/w2m-dsh-plugin",
3
- "version": "1.1.15",
3
+ "version": "1.1.16",
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
  "main": "lib/index.js",
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.1.15",
4
+ "version": "1.1.16",
5
5
  "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.",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -1,4 +1,4 @@
1
- /**
1
+ /**
2
2
  * DSH: Windows2MacO - the DSH Cordis plugin.
3
3
  *
4
4
  * # What this exposes
@@ -214,7 +214,7 @@ const MAX_STAGES = 16;
214
214
  * `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
215
215
  * overrides it at runtime for tests and packagers.
216
216
  */
217
- const PLUGIN_VERSION = '1.1.15';
217
+ const PLUGIN_VERSION = '1.1.16';
218
218
 
219
219
  /** How long any single HTTP call may take before it is aborted, in milliseconds. */
220
220
  const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
@@ -6138,6 +6138,39 @@ export async function apply(ctx, config = {}) {
6138
6138
  }
6139
6139
  const actor = { id: typeof args?.actor_id === 'string' && args.actor_id.trim() !== '' ? args.actor_id.trim() : cfg.machineName, level: args.actor_level };
6140
6140
  const outcome = action === 'approve' ? 'approved' : 'rejected';
6141
+ /**
6142
+ * v1.1.16: a fleet-admission request is settled at the RELAY, because that is where the request lives and where
6143
+ * the quorum rules are enforced. The local index would accept the write and change nothing that matters.
6144
+ *
6145
+ * The actor id is the person answering; the relay records it on the decision. A refusal is reported as such
6146
+ * (`ALREADY_DECIDED` included) rather than thrown: the caller is looking at a list that may be a second old.
6147
+ */
6148
+ if (/^adm-/.test(approvalId)) {
6149
+ const decideDevice = await readDevice(cfg);
6150
+ const decideRelay = cfg.rabbitUrl ?? null;
6151
+ const settled = decideRelay === null || decideDevice.device_token === null
6152
+ ? { ok: false, json: { code: 'W2M_NO_RELAY' } }
6153
+ : await request({
6154
+ rabbitUrl: decideRelay,
6155
+ token: requireToken(decideDevice),
6156
+ method: 'POST',
6157
+ pathname: '/v1/fleet/admission/' + encodeURIComponent(approvalId) + '/decide',
6158
+ body: { decision: action === 'approve' ? 'approve' : 'reject', decided_by: actor.id, auto: false },
6159
+ });
6160
+ return JSON.stringify({
6161
+ ok: settled?.ok === true,
6162
+ action,
6163
+ approval_id: approvalId,
6164
+ kind: 'fleet_admission',
6165
+ settled: settled?.ok === true ? outcome : null,
6166
+ reason_code: settled?.ok === true ? null : (settled?.json?.error?.code ?? settled?.json?.code ?? settled?.error?.code ?? null),
6167
+ detail: settled?.ok === true ? null : (settled?.json?.error?.message ?? null),
6168
+ acted_by: actor,
6169
+ note: settled?.ok === true
6170
+ ? 'the machine is admitted; it will appear in w2m_devices once its agent reconnects'
6171
+ : 'the decision was refused; the request is unchanged (already decided requests are normal when the ten-second window has passed)',
6172
+ });
6173
+ }
6141
6174
  const result = queue.decide(approvalId, actor, outcome);
6142
6175
  return JSON.stringify({
6143
6176
  ok: result.ok === true,
@@ -6161,6 +6194,43 @@ export async function apply(ctx, config = {}) {
6161
6194
  };
6162
6195
  // An actor with no level still gets the list: knowing what is waiting does not require the authority to
6163
6196
  // answer it, and a queue only its supervisors can see is a queue that stays full.
6197
+ /**
6198
+ * v1.1.16: a machine waiting to join the fleet is a request waiting for a human, which is what this tool is.
6199
+ *
6200
+ * It is NOT a permission escalation, so it must not be shoved through `approvalRequestFor` -- that shape requires
6201
+ * a needs-approval verdict and a capability, and faking one would record a lie in the audit trail. The list is
6202
+ * shared; the store underneath is not.
6203
+ *
6204
+ * The countdown uses the RELAY's clock: `requested_at_ms` was stamped there, and three machines in this fleet
6205
+ * disagree on the time by tens of seconds. A local-clock countdown would fire the automatic approval early on a
6206
+ * fast machine and late on a slow one. The relay still owns the decision (it refuses to auto-approve with no
6207
+ * member online); this is display only.
6208
+ */
6209
+ let admissionRows = [];
6210
+ try {
6211
+ const admissionDevice = await readDevice(cfg);
6212
+ const admissionRelay = cfg.rabbitUrl ?? null;
6213
+ // A relay URL and a device token are what the rest of this file passes on every authenticated call; omitting
6214
+ // them made this request throw into the catch below, which is why the merged rows were silently missing.
6215
+ const admissionPending = admissionRelay === null || admissionDevice.device_token === null
6216
+ ? { ok: false }
6217
+ : await request({ rabbitUrl: admissionRelay, token: requireToken(admissionDevice), method: 'GET', pathname: '/v1/fleet/admission/pending' });
6218
+ if (admissionPending?.ok === true && Array.isArray(admissionPending.json?.pending)) {
6219
+ const relayNow = Date.parse(String(admissionPending.json.rabbit_time ?? '')) || Date.now();
6220
+ admissionRows = admissionPending.json.pending.map((row) => ({
6221
+ approval_id: row.request_id,
6222
+ kind: 'fleet_admission',
6223
+ machine_id: row.machine_id ?? null,
6224
+ machine_name: row.machine_name ?? null,
6225
+ requested_at_ms: row.requested_at_ms ?? null,
6226
+ countdown_ms: Number.isFinite(row.requested_at_ms) ? Math.max(0, 10_000 - (relayNow - row.requested_at_ms)) : null,
6227
+ may_act: true,
6228
+ note: 'approving admits this machine to the fleet; the ten-second window is enforced by the relay and needs a member online',
6229
+ }));
6230
+ }
6231
+ } catch {
6232
+ // The approval list must still work when the relay is unreachable: the permission queue is local.
6233
+ }
6164
6234
  const rows = queue.listFor(
6165
6235
  actor.level === null ? { id: actor.id, level: 0 } : actor,
6166
6236
  { includeSettled: args?.include_settled === true },
@@ -6172,8 +6242,8 @@ export async function apply(ctx, config = {}) {
6172
6242
  as: actor,
6173
6243
  level_known: actor.level !== null,
6174
6244
  summary,
6175
- count: rows.length,
6176
- rows,
6245
+ count: rows.length + admissionRows.length,
6246
+ rows: rows.concat(admissionRows),
6177
6247
  note: actor.level === null
6178
6248
  ? 'no actor_level was supplied, so may_act is reported as if the level were 0: the queue is visible, but whether you may answer any of it is not known'
6179
6249
  : 'may_act reflects the actor_level supplied',