@twinsearth/w2m-dsh-plugin 1.9.17 → 1.9.20
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 +13 -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/1.9.20/RELEASE-1920.json +22 -0
- package/docs/acceptance-ops/2026-10-28/AG-SMOKE.json +24 -0
- package/lib/client.js +240 -104
- 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/relay/state.mjs
CHANGED
|
@@ -1790,6 +1790,17 @@ export class RabbitState {
|
|
|
1790
1790
|
const now = this.nowMs();
|
|
1791
1791
|
const cwdRel = typeof input.cwd_rel === 'string' && input.cwd_rel ? input.cwd_rel : '.';
|
|
1792
1792
|
const baseTree = input.base_tree ?? null;
|
|
1793
|
+
// v2 R5: a joint dispatch must be auditable; a malformed binding is refused, never nulled.
|
|
1794
|
+
for (const key of ['plan_sha256', 'approval_sha256']) {
|
|
1795
|
+
const value = input[key];
|
|
1796
|
+
if (value !== undefined && !(typeof value === 'string' && /^[0-9a-f]{64}$/.test(value))) {
|
|
1797
|
+
throw new ProtocolError('BAD_REQUEST', key + ' must be 64 lowercase hex characters');
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
if (input.step_id !== undefined && !(typeof input.step_id === 'string' && /^[a-z][a-z0-9_-]{0,31}$/.test(input.step_id))) {
|
|
1801
|
+
throw new ProtocolError('BAD_REQUEST', 'step_id must be ^[a-z][a-z0-9_-]{0,31}$');
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1793
1804
|
const derivedCommandHash = isPipeline
|
|
1794
1805
|
? computePipelineCommandHash(stages, SHELL_ID_DIRECT)
|
|
1795
1806
|
: computeCommandHash(commandArgv, SHELL_ID_DIRECT, cwdRel);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ArtifactRef v1 (S4): the control-plane reference to an artifact that lives in a USER sink.
|
|
3
|
+
*
|
|
4
|
+
* Invariants:
|
|
5
|
+
* · `object_id` is random (32 hex), never derived from the content;
|
|
6
|
+
* · the control plane carries no plaintext hash — only `cipher.sha256` and `plain.bytes`;
|
|
7
|
+
* · a ref is metadata-only (≤ 4 KiB) and its fields are a closed set;
|
|
8
|
+
* · with a fleet key, `hmac` binds every field (HMAC-SHA-256 over canonical JSON).
|
|
9
|
+
*/
|
|
10
|
+
import { createHash, createHmac, randomBytes } from 'node:crypto';
|
|
11
|
+
|
|
12
|
+
export const ARTIFACT_REF_VERSION = 1;
|
|
13
|
+
export const MAX_REF_BYTES = 4096;
|
|
14
|
+
export const ARTIFACT_KINDS = Object.freeze(['stdout', 'stderr', 'patch', 'commit', 'reduce-input']);
|
|
15
|
+
const HEX64 = /^[0-9a-f]{64}$/;
|
|
16
|
+
const HEX32 = /^[0-9a-f]{32}$/;
|
|
17
|
+
|
|
18
|
+
const stable = (value) => {
|
|
19
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
|
20
|
+
if (Array.isArray(value)) return '[' + value.map(stable).join(',') + ']';
|
|
21
|
+
return '{' + Object.keys(value).sort().map((key) => JSON.stringify(key) + ':' + stable(value[key])).join(',') + '}';
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const digest = (value) => createHash('sha256').update(stable(value), 'utf8').digest('hex');
|
|
25
|
+
|
|
26
|
+
function hmacOf(ref, hmacKey) {
|
|
27
|
+
return createHmac('sha256', hmacKey).update(stable(ref), 'utf8').digest('base64url');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {{objectId?: string, planSha256?: string|null, approvalSha256?: string|null, taskId: string,
|
|
32
|
+
* stepId?: string|null, machineId: string, kind: string, sinkKind?: 'dir'|'lan-git'|'ssh',
|
|
33
|
+
* relPath: string, cipherSha256: string, cipherBytes: number, plainBytes: number,
|
|
34
|
+
* enc?: object, zip?: object, hmacKey?: string}} input
|
|
35
|
+
*/
|
|
36
|
+
export function createArtifactRef(input) {
|
|
37
|
+
const objectId = typeof input?.objectId === 'string' && input.objectId !== '' ? input.objectId : randomBytes(16).toString('hex');
|
|
38
|
+
if (!HEX32.test(objectId)) return { ok: false, code: 'ARTIFACT_OBJECT_ID_INVALID' };
|
|
39
|
+
if (typeof input?.taskId !== 'string' || input.taskId === '') return { ok: false, code: 'ARTIFACT_TASK_REQUIRED' };
|
|
40
|
+
if (typeof input?.machineId !== 'string' || input.machineId === '') return { ok: false, code: 'ARTIFACT_MACHINE_REQUIRED' };
|
|
41
|
+
if (!ARTIFACT_KINDS.includes(input?.kind)) return { ok: false, code: 'ARTIFACT_KIND_INVALID' };
|
|
42
|
+
if (typeof input?.relPath !== 'string' || input.relPath === '' || input.relPath.includes('..')) return { ok: false, code: 'ARTIFACT_PATH_INVALID' };
|
|
43
|
+
if (!HEX64.test(String(input?.cipherSha256 ?? ''))) return { ok: false, code: 'ARTIFACT_CIPHER_SHA_INVALID' };
|
|
44
|
+
if (!Number.isInteger(input?.cipherBytes) || input.cipherBytes < 0) return { ok: false, code: 'ARTIFACT_CIPHER_BYTES_INVALID' };
|
|
45
|
+
if (!Number.isInteger(input?.plainBytes) || input.plainBytes < 0) return { ok: false, code: 'ARTIFACT_PLAIN_BYTES_INVALID' };
|
|
46
|
+
const ref = {
|
|
47
|
+
v: ARTIFACT_REF_VERSION,
|
|
48
|
+
object_id: objectId,
|
|
49
|
+
plan_sha256: HEX64.test(String(input?.planSha256 ?? '')) ? input.planSha256 : null,
|
|
50
|
+
approval_sha256: HEX64.test(String(input?.approvalSha256 ?? '')) ? input.approvalSha256 : null,
|
|
51
|
+
task_id: input.taskId,
|
|
52
|
+
step_id: typeof input?.stepId === 'string' && input.stepId !== '' ? input.stepId : null,
|
|
53
|
+
machine_id: input.machineId,
|
|
54
|
+
kind: input.kind,
|
|
55
|
+
sink: { kind: input?.sinkKind ?? 'dir', rel_path: input.relPath },
|
|
56
|
+
cipher: { sha256: input.cipherSha256, bytes: input.cipherBytes },
|
|
57
|
+
plain: { bytes: input.plainBytes },
|
|
58
|
+
enc: input?.enc ?? null,
|
|
59
|
+
zip: input?.zip ?? null,
|
|
60
|
+
created_at: new Date().toISOString(),
|
|
61
|
+
};
|
|
62
|
+
if (Buffer.byteLength(JSON.stringify(ref), 'utf8') > MAX_REF_BYTES) return { ok: false, code: 'ARTIFACT_REF_TOO_LARGE' };
|
|
63
|
+
if (typeof input?.hmacKey === 'string' && input.hmacKey !== '') ref.hmac = hmacOf(ref, input.hmacKey);
|
|
64
|
+
return { ok: true, ref };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Verify shape, size, the absence of a plaintext hash, and the HMAC when a key is supplied. */
|
|
68
|
+
export function verifyArtifactRef(ref, options = {}) {
|
|
69
|
+
if (ref === null || typeof ref !== 'object') return { ok: false, code: 'ARTIFACT_REF_INVALID' };
|
|
70
|
+
if (ref.v !== ARTIFACT_REF_VERSION) return { ok: false, code: 'ARTIFACT_VERSION_UNSUPPORTED' };
|
|
71
|
+
if (Buffer.byteLength(JSON.stringify(ref), 'utf8') > MAX_REF_BYTES) return { ok: false, code: 'ARTIFACT_REF_TOO_LARGE' };
|
|
72
|
+
if (!HEX32.test(String(ref.object_id ?? ''))) return { ok: false, code: 'ARTIFACT_OBJECT_ID_INVALID' };
|
|
73
|
+
if (!HEX64.test(String(ref?.cipher?.sha256 ?? ''))) return { ok: false, code: 'ARTIFACT_CIPHER_SHA_INVALID' };
|
|
74
|
+
if (typeof ref?.sink?.rel_path !== 'string' || ref.sink.rel_path.includes('..')) return { ok: false, code: 'ARTIFACT_PATH_INVALID' };
|
|
75
|
+
if (ref?.plain !== undefined && ref.plain !== null && typeof ref.plain === 'object' && 'sha256' in ref.plain) {
|
|
76
|
+
return { ok: false, code: 'ARTIFACT_PLAINTEXT_HASH_FORBIDDEN' };
|
|
77
|
+
}
|
|
78
|
+
for (const key of ['plain_sha256', 'sha256_plain', 'content_sha256']) {
|
|
79
|
+
if (key in ref) return { ok: false, code: 'ARTIFACT_PLAINTEXT_HASH_FORBIDDEN' };
|
|
80
|
+
}
|
|
81
|
+
if (typeof options.hmacKey === 'string' && options.hmacKey !== '') {
|
|
82
|
+
if (typeof ref.hmac !== 'string') return { ok: false, code: 'ARTIFACT_HMAC_MISSING' };
|
|
83
|
+
const { hmac: _ignored, ...body } = ref;
|
|
84
|
+
const expected = hmacOf(body, options.hmacKey);
|
|
85
|
+
if (expected !== ref.hmac) return { ok: false, code: 'ARTIFACT_HMAC_INVALID' };
|
|
86
|
+
}
|
|
87
|
+
return { ok: true, digest: digest(ref) };
|
|
88
|
+
}
|
|
@@ -24,7 +24,8 @@ export const BODY_ALLOWED_KEYS = Object.freeze([
|
|
|
24
24
|
'allow_scope_overlap', 'include_offline', 'plan_sha256', 'step_ids',
|
|
25
25
|
'manifest_sha256', 'executor_machine_id', 'head_commit', 'verifier_machine_id',
|
|
26
26
|
'command_argv', 'command_hash', 'cwd_rel', 'timeout_ms',
|
|
27
|
-
'step_id', 'step_ids', 'machine_id', 'write_scope', 'depends_on',
|
|
27
|
+
'step_id', 'step_ids', 'machine_id', 'write_scope', 'depends_on',
|
|
28
|
+
'task_id', 'mode', 'approval_sha256', 'write',
|
|
28
29
|
]);
|
|
29
30
|
|
|
30
31
|
/** 这些字段一律视为数据面内容,出现在控制消息里即拒绝(R1)。 */
|