@twinsearth/w2m-dsh-plugin 1.9.17 → 1.9.19

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.
@@ -191,7 +191,8 @@ import {
191
191
  import { registerChatRoutes } from './fleet-chat-route.mjs';
192
192
  import { registerExecCapabilityRoutes } from './exec-capability-route.mjs';
193
193
  import { registerPluginUpdateRoutes } from './update-route.mjs';
194
- import { registerOperatorTokenRoute } from './operator-token-route.mjs';
194
+ import { registerOperatorTokenRoute } from './operator-token-route.mjs';
195
+ import { registerJointTools } from './tools-joint.mjs';
195
196
  import { fingerprintOf, readOperatorToken } from './operator-token.mjs';
196
197
  import { registerCaptureRoutes } from './capture-route.mjs';
197
198
  import { defaultExecCapabilityFile } from '../util/exec-capability.mjs';
@@ -285,7 +286,7 @@ const MAX_STAGES = 16;
285
286
  * `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
286
287
  * overrides it at runtime for tests and packagers.
287
288
  */
288
- const PLUGIN_VERSION = '1.9.17';
289
+ const PLUGIN_VERSION = '1.9.19';
289
290
 
290
291
  /** How long any single HTTP call may take before it is aborted, in milliseconds. */
291
292
  const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
@@ -3627,6 +3628,8 @@ export async function apply(ctx, config = {}) {
3627
3628
  // -------------------------------------------------------------------------------------------
3628
3629
  // 1. w2m_device - read-only
3629
3630
  // -------------------------------------------------------------------------------------------
3631
+ registerJointTools(ctx, { defineTool, stateDir: cfg.stateDir, machineName: cfg.machineName, output: jsonOutput });
3632
+
3630
3633
  ctx.tools.register(defineTool({
3631
3634
  name: 'w2m_devices',
3632
3635
  description:
@@ -101,6 +101,10 @@ export function createFleetMessages(options = {}) {
101
101
  if (sessionId === '') return { ok: false, code: 'BAD_SESSION' };
102
102
  const machineId = typeof input?.machine_id === 'string' ? input.machine_id.trim() : '';
103
103
  if (machineId === '') return { ok: false, code: 'BAD_MACHINE' };
104
+ // v1.9.18: chat carries text and slash-command text only; images never travel over the relay.
105
+ const MEDIA_FIELDS = ['image', 'image_url', 'image_base64', 'image_bytes', 'picture', 'photo', 'thumb', 'thumbnail', 'attachment', 'attachments', 'file', 'files', 'data', 'base64', 'blob', 'binary', 'buffer', 'media', 'sticker_image', 'sticker_data', 'gif', 'png', 'jpeg', 'webp'];
106
+ const mediaField = Object.keys(input ?? {}).find((key) => MEDIA_FIELDS.includes(key.toLowerCase()));
107
+ if (mediaField !== undefined) return { ok: false, code: 'CHAT_FIELD_FORBIDDEN', detail: { field: mediaField } };
104
108
  if (typeof input?.text !== 'string') return { ok: false, code: 'NOT_TEXT' };
105
109
  const text = input.text;
106
110
  if (text.trim() === '') return { ok: false, code: 'EMPTY_TEXT' };
@@ -130,6 +134,24 @@ export function createFleetMessages(options = {}) {
130
134
  enc = { v: 1, alg: 'aes-256-gcm', iv: rawEnc.iv, tag: rawEnc.tag };
131
135
  }
132
136
 
137
+ // v1.9.18: a plain-text message may not smuggle an image payload. Encrypted text is exempt:
138
+ // it is base64 ciphertext by design and the relay cannot (and must not) inspect it.
139
+ if (enc === null) {
140
+ if (/data:(?:image|video|audio)\//i.test(text) || /!\[[^\]]*\]\(/i.test(text)) {
141
+ return { ok: false, code: 'TEXT_IMAGE_FORBIDDEN' };
142
+ }
143
+ const block = text.match(/[A-Za-z0-9+/]{256,}={0,2}/);
144
+ if (block !== null) {
145
+ try {
146
+ const head = Buffer.from(block[0].slice(0, 32), 'base64');
147
+ const magic = head.slice(0, 4).toString('latin1');
148
+ if ((head[0] === 0x89 && head.slice(1, 4).toString('latin1') === 'PNG') || (head[0] === 0xff && head[1] === 0xd8) || magic.startsWith('GIF8') || magic.startsWith('RIFF')) {
149
+ return { ok: false, code: 'TEXT_BINARY_FORBIDDEN' };
150
+ }
151
+ } catch { /* not base64 that matters */ }
152
+ }
153
+ }
154
+
133
155
  // v1.2.5 idempotency: replay the first message instead of storing the text twice.
134
156
  const idempotencyKey = typeof input?.idempotency_key === 'string' && input.idempotency_key.trim() !== ''
135
157
  ? input.idempotency_key.trim().slice(0, 64)
@@ -524,6 +524,41 @@ export function aggregate(task, resultRecords = [], opts = {}) {
524
524
  }
525
525
 
526
526
  /** Convenience wrapper: pull the task + its results out of a RabbitState. */
527
+ /** S3: a read-only progress view. phase/progress are live-only (null after a restart). */
528
+ export function taskProgress(state, taskId, opts = {}) {
529
+ const task = state.getTask(taskId);
530
+ if (!task) return null;
531
+ const aggregate = aggregateTask(state, taskId, opts);
532
+ const summary = state.taskSummary(task);
533
+ const machines = aggregate.machines.map((machine) => {
534
+ const lease = task.leases.get(machine.machine_id) ?? null;
535
+ return {
536
+ machine_id: machine.machine_id,
537
+ lease_state: machine.lease_state,
538
+ outcome: machine.outcome,
539
+ exit_code: machine.exit_code ?? null,
540
+ phase: lease?.phase ?? null,
541
+ progress: typeof lease?.progress === 'number' ? lease.progress : null,
542
+ last_heartbeat_at: Number.isFinite(lease?.last_heartbeat_ms) ? rfc3339(lease.last_heartbeat_ms) : null,
543
+ lease_until: Number.isFinite(lease?.lease_until_ms) ? rfc3339(lease.lease_until_ms) : null,
544
+ };
545
+ });
546
+ return {
547
+ task_id: taskId,
548
+ mode: task.mode,
549
+ plan_sha256: task.plan_sha256 ?? null,
550
+ approval_sha256: task.approval_sha256 ?? null,
551
+ step_id: task.step_id ?? null,
552
+ command_hash: aggregate.expected?.command_hash ?? task.command_hash ?? null,
553
+ command_argv_len: Array.isArray(task.command_argv) ? task.command_argv.length : 0,
554
+ status: summary.status,
555
+ verdict: aggregate.status,
556
+ terminal: aggregate.status !== 'pending',
557
+ counts: aggregate.counts,
558
+ waiting_on: summary.waiting_on,
559
+ machines,
560
+ };
561
+ }
527
562
  export function aggregateTask(state, taskId, opts = {}) {
528
563
  const task = state.getTask(taskId);
529
564
  if (!task) throw new ProtocolError('NOT_FOUND', 'unknown task_id', { task_id: taskId });