deepdebug-local-agent 1.3.0 → 1.5.0

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.
Files changed (123) hide show
  1. package/.github/workflows/deploy-gcp.yml +58 -26
  2. package/CHANGELOG.md +6 -0
  3. package/docs/la-protocol-v2.md +102 -0
  4. package/package.json +2 -1
  5. package/scripts/sync-la-protocol-v2-contract.mjs +58 -0
  6. package/src/la-protocol-v2/chunks.js +45 -0
  7. package/src/la-protocol-v2/config.js +27 -0
  8. package/src/la-protocol-v2/content-handlers/audit.js +36 -0
  9. package/src/la-protocol-v2/content-handlers/config.js +48 -0
  10. package/src/la-protocol-v2/content-handlers/egress.js +65 -0
  11. package/src/la-protocol-v2/content-handlers/git-diff.js +92 -0
  12. package/src/la-protocol-v2/content-handlers/handler.js +170 -0
  13. package/src/la-protocol-v2/content-handlers/index.js +6 -0
  14. package/src/la-protocol-v2/content-handlers/install.js +50 -0
  15. package/src/la-protocol-v2/content-handlers/path-guard.js +142 -0
  16. package/src/la-protocol-v2/content-handlers/ref-registry.js +87 -0
  17. package/src/la-protocol-v2/content.js +63 -0
  18. package/src/la-protocol-v2/contract/CONTRACT_SOURCE.json +81 -0
  19. package/src/la-protocol-v2/contract/README.md +103 -0
  20. package/src/la-protocol-v2/contract/ack.schema.json +51 -0
  21. package/src/la-protocol-v2/contract/alert.schema.json +62 -0
  22. package/src/la-protocol-v2/contract/approval.decision.schema.json +57 -0
  23. package/src/la-protocol-v2/contract/audit.event.schema.json +86 -0
  24. package/src/la-protocol-v2/contract/config.update.schema.json +73 -0
  25. package/src/la-protocol-v2/contract/content.request.schema.json +110 -0
  26. package/src/la-protocol-v2/contract/content.response.schema.json +90 -0
  27. package/src/la-protocol-v2/contract/defs.schema.json +191 -0
  28. package/src/la-protocol-v2/contract/envelope.schema.json +89 -0
  29. package/src/la-protocol-v2/contract/error.schema.json +59 -0
  30. package/src/la-protocol-v2/contract/heartbeat.schema.json +100 -0
  31. package/src/la-protocol-v2/contract/hello.schema.json +111 -0
  32. package/src/la-protocol-v2/contract/incident.candidate.schema.json +102 -0
  33. package/src/la-protocol-v2/contract/index.json +166 -0
  34. package/src/la-protocol-v2/contract/job.assign.schema.json +212 -0
  35. package/src/la-protocol-v2/contract/job.cancel.schema.json +52 -0
  36. package/src/la-protocol-v2/contract/job.event.schema.json +115 -0
  37. package/src/la-protocol-v2/contract/job.result.schema.json +100 -0
  38. package/src/la-protocol-v2/contract/log.stats.schema.json +79 -0
  39. package/src/la-protocol-v2/contract/ready.schema.json +106 -0
  40. package/src/la-protocol-v2/contract/register.schema.json +92 -0
  41. package/src/la-protocol-v2/contract/scan.upload.schema.json +59 -0
  42. package/src/la-protocol-v2/contract/scan.uploaded.schema.json +51 -0
  43. package/src/la-protocol-v2/contract/webhook.forward.schema.json +69 -0
  44. package/src/la-protocol-v2/contract.js +90 -0
  45. package/src/la-protocol-v2/errors.js +34 -0
  46. package/src/la-protocol-v2/index.js +8 -0
  47. package/src/la-protocol-v2/link.js +93 -0
  48. package/src/la-protocol-v2/runtime.js +81 -0
  49. package/src/la-protocol-v2/session.js +650 -0
  50. package/src/la-protocol-v2/strict-json.js +47 -0
  51. package/src/server.js +25 -4
  52. package/test/contract/la-protocol-v2/vectors/ack.vectors.json +149 -0
  53. package/test/contract/la-protocol-v2/vectors/alert.vectors.json +101 -0
  54. package/test/contract/la-protocol-v2/vectors/approval.decision.vectors.json +165 -0
  55. package/test/contract/la-protocol-v2/vectors/audit.event.vectors.json +156 -0
  56. package/test/contract/la-protocol-v2/vectors/config.update.vectors.json +189 -0
  57. package/test/contract/la-protocol-v2/vectors/content.request.vectors.json +325 -0
  58. package/test/contract/la-protocol-v2/vectors/content.response.vectors.json +413 -0
  59. package/test/contract/la-protocol-v2/vectors/cross-type.vectors.json +260 -0
  60. package/test/contract/la-protocol-v2/vectors/error.vectors.json +179 -0
  61. package/test/contract/la-protocol-v2/vectors/heartbeat.vectors.json +109 -0
  62. package/test/contract/la-protocol-v2/vectors/hello.vectors.json +224 -0
  63. package/test/contract/la-protocol-v2/vectors/incident.candidate.vectors.json +186 -0
  64. package/test/contract/la-protocol-v2/vectors/job.assign.vectors.json +631 -0
  65. package/test/contract/la-protocol-v2/vectors/job.cancel.vectors.json +133 -0
  66. package/test/contract/la-protocol-v2/vectors/job.event.vectors.json +233 -0
  67. package/test/contract/la-protocol-v2/vectors/job.result.vectors.json +137 -0
  68. package/test/contract/la-protocol-v2/vectors/log.stats.vectors.json +137 -0
  69. package/test/contract/la-protocol-v2/vectors/ready.vectors.json +177 -0
  70. package/test/contract/la-protocol-v2/vectors/register.vectors.json +399 -0
  71. package/test/contract/la-protocol-v2/vectors/scan.upload.vectors.json +137 -0
  72. package/test/contract/la-protocol-v2/vectors/scan.uploaded.vectors.json +101 -0
  73. package/test/contract/la-protocol-v2/vectors/traces/invalid-agent-mismatch.json +140 -0
  74. package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-after-last.json +200 -0
  75. package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-byte-length-mismatch.json +174 -0
  76. package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-last-flag.json +200 -0
  77. package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-out-of-order.json +200 -0
  78. package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-total-changes.json +200 -0
  79. package/test/contract/la-protocol-v2/vectors/traces/invalid-clock-skew.json +46 -0
  80. package/test/contract/la-protocol-v2/vectors/traces/invalid-event-before-ready.json +72 -0
  81. package/test/contract/la-protocol-v2/vectors/traces/invalid-first-frame-not-register.json +48 -0
  82. package/test/contract/la-protocol-v2/vectors/traces/invalid-frame-over-48kib.json +141 -0
  83. package/test/contract/la-protocol-v2/vectors/traces/invalid-la-sends-job-assign.json +173 -0
  84. package/test/contract/la-protocol-v2/vectors/traces/invalid-params-raw-differs-from-payload.json +172 -0
  85. package/test/contract/la-protocol-v2/vectors/traces/invalid-payload-above-limit.json +174 -0
  86. package/test/contract/la-protocol-v2/vectors/traces/invalid-resume-restarts-seq.json +144 -0
  87. package/test/contract/la-protocol-v2/vectors/traces/invalid-seq-gap.json +166 -0
  88. package/test/contract/la-protocol-v2/vectors/traces/invalid-seq-repeated.json +166 -0
  89. package/test/contract/la-protocol-v2/vectors/traces/invalid-sig-tenant-mismatch.json +172 -0
  90. package/test/contract/la-protocol-v2/vectors/traces/invalid-signature-warn-with-enforce-required.json +77 -0
  91. package/test/contract/la-protocol-v2/vectors/traces/invalid-tenant-mismatch.json +140 -0
  92. package/test/contract/la-protocol-v2/vectors/traces/invalid-type-outside-allowlist.json +140 -0
  93. package/test/contract/la-protocol-v2/vectors/traces/valid-content-chunks-in-order.json +224 -0
  94. package/test/contract/la-protocol-v2/vectors/traces/valid-handshake-and-job.json +266 -0
  95. package/test/contract/la-protocol-v2/vectors/traces/valid-resume-continues-seq.json +168 -0
  96. package/test/contract/la-protocol-v2/vectors/traces/valid-retransmit-is-idempotent.json +193 -0
  97. package/test/contract/la-protocol-v2/vectors/webhook.forward.vectors.json +170 -0
  98. package/test/la-protocol-v2/content-handlers.test.js +633 -0
  99. package/test/la-protocol-v2/content-path-guard.test.js +214 -0
  100. package/test/la-protocol-v2/content-wiring.test.js +225 -0
  101. package/test/la-protocol-v2/contract.test.js +131 -0
  102. package/test/la-protocol-v2/helpers/content-rig.js +94 -0
  103. package/test/la-protocol-v2/helpers/la-rig.js +83 -0
  104. package/test/la-protocol-v2/helpers/reference-cp.js +349 -0
  105. package/test/la-protocol-v2/helpers/spawn-agent.js +36 -0
  106. package/test/la-protocol-v2/link.test.js +141 -0
  107. package/test/la-protocol-v2/session-inbound.test.js +279 -0
  108. package/test/la-protocol-v2/session-outbound.test.js +382 -0
  109. package/test/la-protocol-v2/traces.test.js +270 -0
  110. package/test/la-protocol-v2/tunnel-e2e.test.js +124 -0
  111. package/tools/graphify-validation/extras/README.md +19 -0
  112. package/tools/graphify-validation/extras/bymethod.py +35 -0
  113. package/tools/graphify-validation/extras/docker/Dockerfile +16 -0
  114. package/tools/graphify-validation/extras/docker/inner.sh +7 -0
  115. package/tools/graphify-validation/extras/g10.py +59 -0
  116. package/tools/graphify-validation/extras/g3aug.py +49 -0
  117. package/tools/graphify-validation/extras/hybrid.py +34 -0
  118. package/tools/graphify-validation/extras/truth2.py +44 -0
  119. package/tools/interop/LaV2NodeContentInteropTest.java +248 -0
  120. package/tools/interop/LaV2NodeInteropTest.java +266 -0
  121. package/tools/interop/README.md +38 -0
  122. package/tools/interop/la-interop-client.mjs +97 -0
  123. package/tools/interop/la-interop-content-client.mjs +91 -0
@@ -0,0 +1,47 @@
1
+ /**
2
+ * strict-json.js - JSON.parse that refuses duplicate object keys (the gateway refuses them too: with a
3
+ * duplicate, two parsers could disagree about which value a signed field has).
4
+ *
5
+ * parseStrict(text) -> value. Throws on invalid JSON or on a duplicate key at any depth.
6
+ * Implemented as a small scanner for the keys plus the native parser for the values, so the parsed
7
+ * result is exactly what JSON.parse returns.
8
+ */
9
+ export function parseStrict(text) {
10
+ const value = JSON.parse(text); // throws on invalid JSON
11
+ assertNoDuplicateKeys(text);
12
+ return value;
13
+ }
14
+
15
+ function assertNoDuplicateKeys(text) {
16
+ const stack = []; // one Set of keys per open object; null for arrays
17
+ let i = 0;
18
+ const n = text.length;
19
+ let expectKey = false;
20
+ while (i < n) {
21
+ const c = text.charCodeAt(i);
22
+ if (c === 0x22 /* " */) {
23
+ const start = i + 1;
24
+ i++;
25
+ while (i < n) {
26
+ const d = text.charCodeAt(i);
27
+ if (d === 0x5c /* \ */) { i += 2; continue; }
28
+ if (d === 0x22) break;
29
+ i++;
30
+ }
31
+ const top = stack[stack.length - 1];
32
+ if (expectKey && top) {
33
+ const key = JSON.parse(`"${text.slice(start, i)}"`);
34
+ if (top.has(key)) throw new Error("duplicate key");
35
+ top.add(key);
36
+ expectKey = false;
37
+ }
38
+ i++;
39
+ continue;
40
+ }
41
+ if (c === 0x7b /* { */) { stack.push(new Set()); expectKey = true; }
42
+ else if (c === 0x5b /* [ */) { stack.push(null); expectKey = false; }
43
+ else if (c === 0x7d || c === 0x5d) { stack.pop(); expectKey = false; }
44
+ else if (c === 0x2c /* , */) { expectKey = stack.length > 0 && stack[stack.length - 1] !== null; }
45
+ i++;
46
+ }
47
+ }
package/src/server.js CHANGED
@@ -19,6 +19,7 @@ import { getSigningConfig, createSignatureMiddleware, captureRawBody, describeSi
19
19
  import { decideExec, rejectionBody, resolveExecPolicyConfig } from "./utils/exec-policy.js";
20
20
  import { createHash as wsCreateHash } from "node:crypto";
21
21
  import { resolveWsMetadataConfig, createWsMetadataGuard, createConnectionState, createOfflinePolicyGuard } from "./ws-channel/index.js";
22
+ import { resolveProtocolV2Config, resolveAgentId } from "./la-protocol-v2/config.js";
22
23
  import { resolveEffectiveHeaderRoot } from "./utils/root-recovery.js";
23
24
  import { classifyGrepFailure, partialSearchBody } from "./utils/search-outcome.js";
24
25
  import { classifyOpenDestination, extractOpenTenantId, isSafeToRemove } from "./utils/open-gate.js";
@@ -644,6 +645,11 @@ console.log(`[exec-policy] mode=${EXEC_POLICY.mode} (allowlist; LOCAL_AGENT_EXEC
644
645
  // actions are denied while the control plane is unreachable. off = nothing changes.
645
646
  const WS_METADATA = resolveWsMetadataConfig();
646
647
  const WS_CONNECTION = createConnectionState();
648
+ // LOCAL_AGENT_PROTOCOL_V2 (F3-05b, default off): with it off the tunnel below is the v1 client, untouched.
649
+ const PROTOCOL_V2 = resolveProtocolV2Config();
650
+ if (!PROTOCOL_V2.enabled && /^(true|1|yes|on)$/i.test(String(process.env.LOCAL_AGENT_CONTENT_HANDLERS ?? '').trim())) {
651
+ console.warn('[la-v2] LOCAL_AGENT_CONTENT_HANDLERS is set but LOCAL_AGENT_PROTOCOL_V2 is off: content handlers stay off');
652
+ }
647
653
  if (WS_METADATA.mode === "enforce") {
648
654
  EXEC_POLICY.offlineGuard = createOfflinePolicyGuard({
649
655
  connection: WS_CONNECTION,
@@ -6941,7 +6947,7 @@ app.post("/workspace/:workspaceId/run", async (req, res) => {
6941
6947
  // WEBSOCKET REVERSE TUNNEL
6942
6948
  // Connects Local Agent to Gateway no public URL needed
6943
6949
  // ============================================
6944
- async function startWebSocketTunnel(port, gatewayUrl, authToken, tenantId) {
6950
+ async function startWebSocketTunnel(port, gatewayUrl, authToken, tenantId, agentId = null) {
6945
6951
  if (!gatewayUrl || !tenantId) {
6946
6952
  console.log('WebSocket tunnel skipped: missing gatewayUrl or tenantId');
6947
6953
  return;
@@ -6977,6 +6983,16 @@ async function startWebSocketTunnel(port, gatewayUrl, authToken, tenantId) {
6977
6983
  const out = metaGuard ? guardedFrame : JSON.stringify(legacyObj);
6978
6984
  if (out) ws.send(out);
6979
6985
  };
6986
+ // Protocol v2 (F3-05b): only when LOCAL_AGENT_PROTOCOL_V2=true. null = the v1 client exactly as before.
6987
+ let v2Link = null;
6988
+ if (PROTOCOL_V2.enabled) {
6989
+ try {
6990
+ const { buildV2Link } = await import('./la-protocol-v2/runtime.js');
6991
+ v2Link = await buildV2Link({ tenantId, agentId, signing: SIGNING, wsManager, getWorkspaceRoot: () => WORKSPACE_ROOT });
6992
+ } catch (err) {
6993
+ console.warn(`[la-v2] off: ${err.code || err.name} while loading the protocol module`);
6994
+ }
6995
+ }
6980
6996
 
6981
6997
  async function connect() {
6982
6998
  if (isShuttingDown) return;
@@ -6989,10 +7005,12 @@ async function startWebSocketTunnel(port, gatewayUrl, authToken, tenantId) {
6989
7005
  reconnectDelay = 2000;
6990
7006
  WS_CONNECTION.markOpen();
6991
7007
  const legacyRegister = { type: 'register', tenantId, port, workspacePath: WORKSPACE_ROOT || null };
6992
- sendFrame(legacyRegister, metaGuard && metaGuard.frameMessage({
7008
+ const sendLegacyRegister = () => sendFrame(legacyRegister, metaGuard && metaGuard.frameMessage({
6993
7009
  type: 'register', tenantId, port: Number(port),
6994
7010
  ...(WORKSPACE_ROOT ? { workspaceHash: wsCreateHash("sha256").update(String(WORKSPACE_ROOT)).digest('hex') } : {}),
6995
7011
  }));
7012
+ // v2 owns the registration while it negotiates; the v1 register goes out only if the gateway does not speak v2.
7013
+ if (!(v2Link && v2Link.begin(ws, sendLegacyRegister))) sendLegacyRegister();
6996
7014
  const hb = setInterval(() => {
6997
7015
  if (ws.readyState === WebSocket.OPEN) {
6998
7016
  ws.send('ping');
@@ -7005,10 +7023,12 @@ async function startWebSocketTunnel(port, gatewayUrl, authToken, tenantId) {
7005
7023
 
7006
7024
  ws.on('pong', () => WS_CONNECTION.markSeen());
7007
7025
 
7008
- ws.on('message', async (data) => {
7026
+ ws.on('message', async (data, isBinary) => {
7009
7027
  WS_CONNECTION.markSeen();
7010
7028
  const text = data.toString();
7011
7029
  if (text === 'pong') return;
7030
+ // v2 frames (schemaVersion) are handled by the protocol module; v1 commands fall through to the handler below.
7031
+ if (v2Link && v2Link.handleMessage(ws, text, isBinary)) return;
7012
7032
  let request;
7013
7033
  try { request = JSON.parse(text); } catch { return; }
7014
7034
  const { requestId, command } = request;
@@ -7035,6 +7055,7 @@ async function startWebSocketTunnel(port, gatewayUrl, authToken, tenantId) {
7035
7055
 
7036
7056
  ws.on('close', () => {
7037
7057
  WS_CONNECTION.markClosed();
7058
+ if (v2Link) v2Link.onClose(ws);
7038
7059
  if (!isShuttingDown) {
7039
7060
  console.log(`WS disconnected. Reconnecting in ${reconnectDelay/1000}s...`);
7040
7061
  setTimeout(connect, reconnectDelay);
@@ -8164,7 +8185,7 @@ app.listen(PORT, BIND_HOST, async () => {
8164
8185
  }
8165
8186
 
8166
8187
  if (process.env.DISABLE_WS_TUNNEL !== 'true') {
8167
- startWebSocketTunnel(PORT, gwUrl, wsToken, tenantId);
8188
+ startWebSocketTunnel(PORT, gwUrl, wsToken, tenantId, resolveAgentId(process.env, cfg));
8168
8189
  }
8169
8190
 
8170
8191
  // Sibling plane (started here so the frozen monolith is untouched): metrics
@@ -0,0 +1,149 @@
1
+ {
2
+ "type": "ack",
3
+ "schema": "ack.schema.json",
4
+ "vectors": [
5
+ {
6
+ "name": "minimal-valid",
7
+ "expect": {
8
+ "valid": true
9
+ },
10
+ "message": {
11
+ "schemaVersion": 2,
12
+ "type": "ack",
13
+ "msgId": "00000000-0000-4000-8000-000000000001",
14
+ "seq": 0,
15
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
16
+ "workspaceId": "ws-1",
17
+ "ts": "2026-10-29T10:00:00Z",
18
+ "payload": {
19
+ "ackSeq": 1,
20
+ "status": "accepted"
21
+ },
22
+ "agentId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
23
+ }
24
+ },
25
+ {
26
+ "name": "extra-top-level-field",
27
+ "why": "envelope is closed (additionalProperties:false)",
28
+ "expect": {
29
+ "valid": false,
30
+ "code": "schema_violation",
31
+ "keyword": "additionalProperties"
32
+ },
33
+ "message": {
34
+ "schemaVersion": 2,
35
+ "type": "ack",
36
+ "msgId": "00000000-0000-4000-8000-000000000001",
37
+ "seq": 0,
38
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
39
+ "workspaceId": "ws-1",
40
+ "ts": "2026-10-29T10:00:00Z",
41
+ "payload": {
42
+ "ackSeq": 1,
43
+ "status": "accepted"
44
+ },
45
+ "extra": "x",
46
+ "agentId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
47
+ }
48
+ },
49
+ {
50
+ "name": "extra-payload-field",
51
+ "why": "payload is closed: no free-form fields",
52
+ "expect": {
53
+ "valid": false,
54
+ "code": "schema_violation",
55
+ "keyword": "additionalProperties"
56
+ },
57
+ "message": {
58
+ "schemaVersion": 2,
59
+ "type": "ack",
60
+ "msgId": "00000000-0000-4000-8000-000000000001",
61
+ "seq": 0,
62
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
63
+ "workspaceId": "ws-1",
64
+ "ts": "2026-10-29T10:00:00Z",
65
+ "payload": {
66
+ "ackSeq": 1,
67
+ "status": "accepted",
68
+ "freeText": "client text"
69
+ },
70
+ "agentId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
71
+ }
72
+ },
73
+ {
74
+ "name": "tenant-id-not-uuid",
75
+ "why": "tenantId must be a UUID",
76
+ "expect": {
77
+ "valid": false,
78
+ "code": "schema_violation",
79
+ "keyword": "pattern"
80
+ },
81
+ "message": {
82
+ "schemaVersion": 2,
83
+ "type": "ack",
84
+ "msgId": "00000000-0000-4000-8000-000000000001",
85
+ "seq": 0,
86
+ "tenantId": "tenant-1",
87
+ "workspaceId": "ws-1",
88
+ "ts": "2026-10-29T10:00:00Z",
89
+ "payload": {
90
+ "ackSeq": 1,
91
+ "status": "accepted"
92
+ },
93
+ "agentId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
94
+ }
95
+ },
96
+ {
97
+ "name": "ack-with-sig",
98
+ "why": "control frames are not signed",
99
+ "expect": {
100
+ "valid": false,
101
+ "code": "schema_violation"
102
+ },
103
+ "message": {
104
+ "schemaVersion": 2,
105
+ "type": "ack",
106
+ "msgId": "00000000-0000-4000-8000-000000000001",
107
+ "seq": 0,
108
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
109
+ "workspaceId": "ws-1",
110
+ "ts": "2026-10-29T10:00:00Z",
111
+ "payload": {
112
+ "ackSeq": 1,
113
+ "status": "accepted"
114
+ },
115
+ "sig": {
116
+ "keyId": "vh-cp-2026-10",
117
+ "timestamp": "1793268000",
118
+ "nonce": "Zm9vYmFyYmF6cXV4MTIzNDU2",
119
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
120
+ "workspaceId": "ws-1",
121
+ "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
122
+ },
123
+ "agentId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
124
+ }
125
+ },
126
+ {
127
+ "name": "missing-agent-id",
128
+ "expect": {
129
+ "valid": false,
130
+ "code": "schema_violation",
131
+ "keyword": "required"
132
+ },
133
+ "message": {
134
+ "schemaVersion": 2,
135
+ "type": "ack",
136
+ "msgId": "00000000-0000-4000-8000-000000000001",
137
+ "seq": 0,
138
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
139
+ "workspaceId": "ws-1",
140
+ "ts": "2026-10-29T10:00:00Z",
141
+ "payload": {
142
+ "ackSeq": 1,
143
+ "status": "accepted"
144
+ }
145
+ },
146
+ "why": "agentId is required on ack/error (the gateway refuses a frame without it: agent_mismatch)"
147
+ }
148
+ ]
149
+ }
@@ -0,0 +1,101 @@
1
+ {
2
+ "type": "alert",
3
+ "schema": "alert.schema.json",
4
+ "vectors": [
5
+ {
6
+ "name": "minimal-valid",
7
+ "expect": {
8
+ "valid": true
9
+ },
10
+ "message": {
11
+ "schemaVersion": 2,
12
+ "type": "alert",
13
+ "msgId": "00000000-0000-4000-8000-000000000001",
14
+ "seq": 1,
15
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
16
+ "workspaceId": "ws-1",
17
+ "ts": "2026-10-29T10:00:00Z",
18
+ "agentId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
19
+ "payload": {
20
+ "code": "index_stale",
21
+ "severity": "warning",
22
+ "value": 86400
23
+ }
24
+ }
25
+ },
26
+ {
27
+ "name": "extra-top-level-field",
28
+ "why": "envelope is closed (additionalProperties:false)",
29
+ "expect": {
30
+ "valid": false,
31
+ "code": "schema_violation",
32
+ "keyword": "additionalProperties"
33
+ },
34
+ "message": {
35
+ "schemaVersion": 2,
36
+ "type": "alert",
37
+ "msgId": "00000000-0000-4000-8000-000000000001",
38
+ "seq": 1,
39
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
40
+ "workspaceId": "ws-1",
41
+ "ts": "2026-10-29T10:00:00Z",
42
+ "agentId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
43
+ "payload": {
44
+ "code": "index_stale",
45
+ "severity": "warning",
46
+ "value": 86400
47
+ },
48
+ "extra": "x"
49
+ }
50
+ },
51
+ {
52
+ "name": "extra-payload-field",
53
+ "why": "payload is closed: no free-form fields",
54
+ "expect": {
55
+ "valid": false,
56
+ "code": "schema_violation",
57
+ "keyword": "additionalProperties"
58
+ },
59
+ "message": {
60
+ "schemaVersion": 2,
61
+ "type": "alert",
62
+ "msgId": "00000000-0000-4000-8000-000000000001",
63
+ "seq": 1,
64
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
65
+ "workspaceId": "ws-1",
66
+ "ts": "2026-10-29T10:00:00Z",
67
+ "agentId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
68
+ "payload": {
69
+ "code": "index_stale",
70
+ "severity": "warning",
71
+ "value": 86400,
72
+ "freeText": "client text"
73
+ }
74
+ }
75
+ },
76
+ {
77
+ "name": "tenant-id-not-uuid",
78
+ "why": "tenantId must be a UUID",
79
+ "expect": {
80
+ "valid": false,
81
+ "code": "schema_violation",
82
+ "keyword": "pattern"
83
+ },
84
+ "message": {
85
+ "schemaVersion": 2,
86
+ "type": "alert",
87
+ "msgId": "00000000-0000-4000-8000-000000000001",
88
+ "seq": 1,
89
+ "tenantId": "tenant-1",
90
+ "workspaceId": "ws-1",
91
+ "ts": "2026-10-29T10:00:00Z",
92
+ "agentId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
93
+ "payload": {
94
+ "code": "index_stale",
95
+ "severity": "warning",
96
+ "value": 86400
97
+ }
98
+ }
99
+ }
100
+ ]
101
+ }
@@ -0,0 +1,165 @@
1
+ {
2
+ "type": "approval.decision",
3
+ "schema": "approval.decision.schema.json",
4
+ "vectors": [
5
+ {
6
+ "name": "minimal-valid",
7
+ "expect": {
8
+ "valid": true
9
+ },
10
+ "message": {
11
+ "schemaVersion": 2,
12
+ "type": "approval.decision",
13
+ "msgId": "00000000-0000-4000-8000-000000000001",
14
+ "seq": 1,
15
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
16
+ "workspaceId": "ws-1",
17
+ "ts": "2026-10-29T10:00:00Z",
18
+ "paramsRaw": "{\"approvalId\":\"d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80\",\"decision\":\"approved\",\"token\":\"vhapr1.k1.eyJ0eXAiOiJ2aC1odW1hbi1hcHByb3ZhbCJ9.c2lnbmF0dXJl\"}",
19
+ "sig": {
20
+ "keyId": "vh-cp-2026-10",
21
+ "timestamp": "1793268000",
22
+ "nonce": "Zm9vYmFyYmF6cXV4MTIzNDU2",
23
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
24
+ "workspaceId": "ws-1",
25
+ "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
26
+ },
27
+ "payload": {
28
+ "approvalId": "d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80",
29
+ "decision": "approved",
30
+ "token": "vhapr1.k1.eyJ0eXAiOiJ2aC1odW1hbi1hcHByb3ZhbCJ9.c2lnbmF0dXJl"
31
+ }
32
+ }
33
+ },
34
+ {
35
+ "name": "extra-top-level-field",
36
+ "why": "envelope is closed (additionalProperties:false)",
37
+ "expect": {
38
+ "valid": false,
39
+ "code": "schema_violation",
40
+ "keyword": "additionalProperties"
41
+ },
42
+ "message": {
43
+ "schemaVersion": 2,
44
+ "type": "approval.decision",
45
+ "msgId": "00000000-0000-4000-8000-000000000001",
46
+ "seq": 1,
47
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
48
+ "workspaceId": "ws-1",
49
+ "ts": "2026-10-29T10:00:00Z",
50
+ "paramsRaw": "{\"approvalId\":\"d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80\",\"decision\":\"approved\",\"token\":\"vhapr1.k1.eyJ0eXAiOiJ2aC1odW1hbi1hcHByb3ZhbCJ9.c2lnbmF0dXJl\"}",
51
+ "sig": {
52
+ "keyId": "vh-cp-2026-10",
53
+ "timestamp": "1793268000",
54
+ "nonce": "Zm9vYmFyYmF6cXV4MTIzNDU2",
55
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
56
+ "workspaceId": "ws-1",
57
+ "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
58
+ },
59
+ "payload": {
60
+ "approvalId": "d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80",
61
+ "decision": "approved",
62
+ "token": "vhapr1.k1.eyJ0eXAiOiJ2aC1odW1hbi1hcHByb3ZhbCJ9.c2lnbmF0dXJl"
63
+ },
64
+ "extra": "x"
65
+ }
66
+ },
67
+ {
68
+ "name": "extra-payload-field",
69
+ "why": "payload is closed: no free-form fields",
70
+ "expect": {
71
+ "valid": false,
72
+ "code": "schema_violation",
73
+ "keyword": "additionalProperties"
74
+ },
75
+ "message": {
76
+ "schemaVersion": 2,
77
+ "type": "approval.decision",
78
+ "msgId": "00000000-0000-4000-8000-000000000001",
79
+ "seq": 1,
80
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
81
+ "workspaceId": "ws-1",
82
+ "ts": "2026-10-29T10:00:00Z",
83
+ "paramsRaw": "{\"approvalId\":\"d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80\",\"decision\":\"approved\",\"token\":\"vhapr1.k1.eyJ0eXAiOiJ2aC1odW1hbi1hcHByb3ZhbCJ9.c2lnbmF0dXJl\"}",
84
+ "sig": {
85
+ "keyId": "vh-cp-2026-10",
86
+ "timestamp": "1793268000",
87
+ "nonce": "Zm9vYmFyYmF6cXV4MTIzNDU2",
88
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
89
+ "workspaceId": "ws-1",
90
+ "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
91
+ },
92
+ "payload": {
93
+ "approvalId": "d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80",
94
+ "decision": "approved",
95
+ "token": "vhapr1.k1.eyJ0eXAiOiJ2aC1odW1hbi1hcHByb3ZhbCJ9.c2lnbmF0dXJl",
96
+ "freeText": "client text"
97
+ }
98
+ }
99
+ },
100
+ {
101
+ "name": "tenant-id-not-uuid",
102
+ "why": "tenantId must be a UUID",
103
+ "expect": {
104
+ "valid": false,
105
+ "code": "schema_violation",
106
+ "keyword": "pattern"
107
+ },
108
+ "message": {
109
+ "schemaVersion": 2,
110
+ "type": "approval.decision",
111
+ "msgId": "00000000-0000-4000-8000-000000000001",
112
+ "seq": 1,
113
+ "tenantId": "tenant-1",
114
+ "workspaceId": "ws-1",
115
+ "ts": "2026-10-29T10:00:00Z",
116
+ "paramsRaw": "{\"approvalId\":\"d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80\",\"decision\":\"approved\",\"token\":\"vhapr1.k1.eyJ0eXAiOiJ2aC1odW1hbi1hcHByb3ZhbCJ9.c2lnbmF0dXJl\"}",
117
+ "sig": {
118
+ "keyId": "vh-cp-2026-10",
119
+ "timestamp": "1793268000",
120
+ "nonce": "Zm9vYmFyYmF6cXV4MTIzNDU2",
121
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
122
+ "workspaceId": "ws-1",
123
+ "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
124
+ },
125
+ "payload": {
126
+ "approvalId": "d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80",
127
+ "decision": "approved",
128
+ "token": "vhapr1.k1.eyJ0eXAiOiJ2aC1odW1hbi1hcHByb3ZhbCJ9.c2lnbmF0dXJl"
129
+ }
130
+ }
131
+ },
132
+ {
133
+ "name": "token-not-vhapr1",
134
+ "why": "token must be a vhapr1 token",
135
+ "expect": {
136
+ "valid": false,
137
+ "code": "schema_violation",
138
+ "keyword": "pattern"
139
+ },
140
+ "message": {
141
+ "schemaVersion": 2,
142
+ "type": "approval.decision",
143
+ "msgId": "00000000-0000-4000-8000-000000000001",
144
+ "seq": 1,
145
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
146
+ "workspaceId": "ws-1",
147
+ "ts": "2026-10-29T10:00:00Z",
148
+ "paramsRaw": "{\"approvalId\":\"d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80\",\"decision\":\"approved\",\"token\":\"vhapr1.k1.eyJ0eXAiOiJ2aC1odW1hbi1hcHByb3ZhbCJ9.c2lnbmF0dXJl\"}",
149
+ "sig": {
150
+ "keyId": "vh-cp-2026-10",
151
+ "timestamp": "1793268000",
152
+ "nonce": "Zm9vYmFyYmF6cXV4MTIzNDU2",
153
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
154
+ "workspaceId": "ws-1",
155
+ "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
156
+ },
157
+ "payload": {
158
+ "approvalId": "d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80",
159
+ "decision": "approved",
160
+ "token": "bearer abc"
161
+ }
162
+ }
163
+ }
164
+ ]
165
+ }