@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.
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +6 -0
- package/README.md +3 -3
- package/bin/w2m-joint.mjs +1 -1
- package/codex/mcp-servers.toml +1 -1
- package/docs/acceptance/1.9.19/RELEASE-1919.json +21 -0
- package/docs/acceptance-ops/2026-10-28/AG-SMOKE.json +24 -0
- package/lib/client.js +185 -105
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/src/agent/agent.mjs +6 -1
- package/src/agent/sinks/dir-sink.mjs +59 -13
- package/src/plugin/fleet-chat-route.mjs +7 -1
- package/src/plugin/joint-dev/audit-file.mjs +70 -37
- package/src/plugin/joint-dev/merge-engine.mjs +102 -0
- package/src/plugin/joint-dev/relay-dispatch.mjs +103 -21
- package/src/plugin/tools-joint.mjs +97 -0
- package/src/plugin/tools.mjs +5 -2
- package/src/relay/fleet-messages.mjs +22 -0
- package/src/relay/report.mjs +35 -0
- package/src/relay/server.mjs +2043 -2038
- package/src/relay/state.mjs +11 -0
- package/src/util/artifact-ref.mjs +88 -0
- package/src/util/redline-core.mjs +2 -1
package/src/plugin/tools.mjs
CHANGED
|
@@ -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.
|
|
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)
|
package/src/relay/report.mjs
CHANGED
|
@@ -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 });
|